From 73848b6b1a89f9c974e2ef3fe7e6defc9f8c9d3a Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Mar 2026 13:34:25 -0700 Subject: [PATCH 1/9] Add Jupyter notebook cell support via LSP 3.17 Notebook Document Sync - Add NOTEBOOK_SYNC_OPTIONS and notebook_document_sync to LanguageServer - Add _get_document_path helper for resolving notebook cell URIs - Add notebook lifecycle handlers (didOpen, didChange, didSave, didClose) - Remove notebook-cell skip guard in _linting_helper - Add onNotebook:jupyter-notebook and onNotebook:interactive activation events Closes #86 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bundled/tool/lsp_server.py | 103 ++++++++++++++++++++++++++++++++++--- package.json | 4 +- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/bundled/tool/lsp_server.py b/bundled/tool/lsp_server.py index c0236ba8..8788347f 100644 --- a/bundled/tool/lsp_server.py +++ b/bundled/tool/lsp_server.py @@ -15,6 +15,7 @@ import uuid from dataclasses import dataclass from typing import Any, Dict, List, Optional, Sequence +from urllib.parse import urlparse, urlunparse # ********************************************************** @@ -75,11 +76,43 @@ def update_environ_path() -> None: GLOBAL_SETTINGS = {} MAX_WORKERS = 5 -LSP_SERVER = LanguageServer(name="Mypy", version="v0.1.0", max_workers=MAX_WORKERS) +NOTEBOOK_SYNC_OPTIONS = lsp.NotebookDocumentSyncOptions( + notebook_selector=[ + lsp.NotebookDocumentFilterWithNotebook( + notebook="jupyter-notebook", + cells=[ + lsp.NotebookCellLanguage(language="python"), + ], + ), + lsp.NotebookDocumentFilterWithNotebook( + notebook="interactive", + cells=[ + lsp.NotebookCellLanguage(language="python"), + ], + ), + ], + save=True, +) +LSP_SERVER = LanguageServer(name="Mypy", version="v0.1.0", max_workers=MAX_WORKERS, notebook_document_sync=NOTEBOOK_SYNC_OPTIONS) DMYPY_ARGS = {} DMYPY_STATUS_FILE_ROOT = None + +def _get_document_path(document: TextDocument) -> str: + """Returns the filesystem path for a document. + + Examples: + file:///path/to/file.py -> /path/to/file.py + vscode-notebook-cell:/path/to/notebook.ipynb#C00001 -> /path/to/notebook.ipynb + """ + if not document.uri.startswith("file:"): + parsed = urlparse(document.uri) + file_uri = urlunparse(("file", *parsed[1:-1], "")) + if result := uris.to_fs_path(file_uri): + return result + return document.path + # ********************************************************** # Tool specific code goes below this. # ********************************************************** @@ -183,6 +216,68 @@ def did_close(params: lsp.DidCloseTextDocumentParams) -> None: _clear_diagnostics(document) +@LSP_SERVER.feature(lsp.NOTEBOOK_DOCUMENT_DID_OPEN) +def notebook_did_open(params: lsp.DidOpenNotebookDocumentParams) -> None: + """Run diagnostics on each code cell when a notebook is opened.""" + nb = LSP_SERVER.workspace.get_notebook_document( + notebook_uri=params.notebook_document.uri + ) + if nb is None: + return + for cell in nb.cells: + if cell.kind != lsp.NotebookCellKind.Code or cell.document is None: + continue + document = LSP_SERVER.workspace.get_text_document(cell.document) + _linting_helper(document) + + +@LSP_SERVER.feature(lsp.NOTEBOOK_DOCUMENT_DID_CHANGE) +def notebook_did_change(params: lsp.DidChangeNotebookDocumentParams) -> None: + """Re-lint cells whose text changed or that were newly added.""" + if params.change is None or params.change.cells is None: + return + + for cell_content in params.change.cells.text_content or []: + document = LSP_SERVER.workspace.get_text_document(cell_content.document.uri) + _linting_helper(document) + + structure = params.change.cells.structure + if structure and structure.did_open: + for cell_doc in structure.did_open: + document = LSP_SERVER.workspace.get_text_document(cell_doc.uri) + _linting_helper(document) + + if structure and structure.did_close: + for cell_doc in structure.did_close: + LSP_SERVER.text_document_publish_diagnostics( + lsp.PublishDiagnosticsParams(uri=cell_doc.uri, diagnostics=[]) + ) + + +@LSP_SERVER.feature(lsp.NOTEBOOK_DOCUMENT_DID_SAVE) +def notebook_did_save(params: lsp.DidSaveNotebookDocumentParams) -> None: + """Re-lint all cells when a notebook is saved.""" + nb = LSP_SERVER.workspace.get_notebook_document( + notebook_uri=params.notebook_document.uri + ) + if nb is None: + return + for cell in nb.cells: + if cell.kind != lsp.NotebookCellKind.Code or cell.document is None: + continue + document = LSP_SERVER.workspace.get_text_document(cell.document) + _linting_helper(document) + + +@LSP_SERVER.feature(lsp.NOTEBOOK_DOCUMENT_DID_CLOSE) +def notebook_did_close(params: lsp.DidCloseNotebookDocumentParams) -> None: + """Clear diagnostics for all cells when the notebook is closed.""" + for cell_doc in params.cell_text_documents: + LSP_SERVER.text_document_publish_diagnostics( + lsp.PublishDiagnosticsParams(uri=cell_doc.uri, diagnostics=[]) + ) + + def _is_empty_diagnostics( filepath: str, results: Optional[Dict[str, str | None]] ) -> bool: @@ -210,12 +305,6 @@ def _linting_helper(document: TextDocument) -> None: # deep copy here to prevent accidentally updating global settings. settings = copy.deepcopy(_get_settings_by_document(document)) - if str(document.uri).startswith("vscode-notebook-cell"): - # We don't support running mypy on notebook cells. - log_warning(f"Skipping notebook cells [Not Supported]: {str(document.uri)}") - _clear_diagnostics(document) - return None - if settings["reportingScope"] == "file" and utils.is_stdlib_file(document.path): log_warning( f"Skipping standard library file (stdlib excluded): {document.path}" diff --git a/package.json b/package.json index da9a627f..d814eecc 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,9 @@ }, "activationEvents": [ "onLanguage:python", - "workspaceContains:mypy.ini" + "workspaceContains:mypy.ini", + "onNotebook:jupyter-notebook", + "onNotebook:interactive" ], "main": "./dist/extension.js", "scripts": { From 56ac8e90107072cc7e0dd0f5fe82dfbabf465924 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Mar 2026 13:49:47 -0700 Subject: [PATCH 2/9] Add notebook mock types to test_get_cwd.py Add NOTEBOOK_DOCUMENT_DID_* event constants and notebook-related type classes (NotebookDocumentSyncOptions, NotebookCellLanguage, etc.) to the lsprotocol.types mock so lsp_server.py can be imported in unit tests. Also update _MockLS to use Pygls 2.0 method names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/test/python_tests/test_get_cwd.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/test/python_tests/test_get_cwd.py b/src/test/python_tests/test_get_cwd.py index 437e2392..90af14da 100644 --- a/src/test/python_tests/test_get_cwd.py +++ b/src/test/python_tests/test_get_cwd.py @@ -25,13 +25,10 @@ def feature(self, *args, **kwargs): def command(self, *args, **kwargs): return lambda f: f - def show_message_log(self, *args, **kwargs): - pass - - def show_message(self, *args, **kwargs): + def window_log_message(self, *args, **kwargs): pass - def window_log_message(self, *args, **kwargs): + def window_show_message(self, *args, **kwargs): pass mock_server = types.ModuleType("pygls.lsp.server") @@ -53,6 +50,10 @@ def window_log_message(self, *args, **kwargs): "INITIALIZE", "EXIT", "SHUTDOWN", + "NOTEBOOK_DOCUMENT_DID_OPEN", + "NOTEBOOK_DOCUMENT_DID_CHANGE", + "NOTEBOOK_DOCUMENT_DID_SAVE", + "NOTEBOOK_DOCUMENT_DID_CLOSE", ]: setattr(mock_lsp, _name, _name) for _name in [ @@ -61,10 +62,19 @@ def window_log_message(self, *args, **kwargs): "DidCloseTextDocumentParams", "DidOpenTextDocumentParams", "DidSaveTextDocumentParams", + "DidChangeNotebookDocumentParams", + "DidCloseNotebookDocumentParams", + "DidOpenNotebookDocumentParams", + "DidSaveNotebookDocumentParams", "DocumentFormattingParams", "InitializeParams", "LogMessageParams", + "NotebookCellKind", + "NotebookCellLanguage", + "NotebookDocumentFilterWithNotebook", + "NotebookDocumentSyncOptions", "Position", + "PublishDiagnosticsParams", "Range", "TextEdit", ]: From 0d53a3407f9e315b76fbc83a7f9fba18dc3adce5 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Mar 2026 13:56:45 -0700 Subject: [PATCH 3/9] Fix E305 linting: add blank line after _get_document_path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bundled/tool/lsp_server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bundled/tool/lsp_server.py b/bundled/tool/lsp_server.py index 8788347f..0eda192a 100644 --- a/bundled/tool/lsp_server.py +++ b/bundled/tool/lsp_server.py @@ -113,6 +113,7 @@ def _get_document_path(document: TextDocument) -> str: return result return document.path + # ********************************************************** # Tool specific code goes below this. # ********************************************************** From 5f1a01874b24c2b361749f9159790ae80e906bd6 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Mar 2026 14:44:20 -0700 Subject: [PATCH 4/9] Add notebook LSP test infrastructure Add test_notebook.py with 5 tests covering notebook lifecycle events (didOpen, didChange, didSave, didClose, cell kind filter). Add notify_notebook_did_* helpers to session.py test client. Add sample.ipynb test data fixture. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../python_tests/lsp_test_client/session.py | 16 + .../test_data/sample1/sample.ipynb | 27 ++ src/test/python_tests/test_notebook.py | 426 ++++++++++++++++++ 3 files changed, 469 insertions(+) create mode 100644 src/test/python_tests/test_data/sample1/sample.ipynb create mode 100644 src/test/python_tests/test_notebook.py diff --git a/src/test/python_tests/lsp_test_client/session.py b/src/test/python_tests/lsp_test_client/session.py index 060577d4..0b7708cc 100644 --- a/src/test/python_tests/lsp_test_client/session.py +++ b/src/test/python_tests/lsp_test_client/session.py @@ -142,6 +142,22 @@ def notify_did_close(self, did_close_params): """Sends did close notification to LSP Server.""" self._send_notification("textDocument/didClose", params=did_close_params) + def notify_notebook_did_open(self, params): + """Sends notebookDocument/didOpen notification to LSP Server.""" + self._send_notification("notebookDocument/didOpen", params=params) + + def notify_notebook_did_change(self, params): + """Sends notebookDocument/didChange notification to LSP Server.""" + self._send_notification("notebookDocument/didChange", params=params) + + def notify_notebook_did_save(self, params): + """Sends notebookDocument/didSave notification to LSP Server.""" + self._send_notification("notebookDocument/didSave", params=params) + + def notify_notebook_did_close(self, params): + """Sends notebookDocument/didClose notification to LSP Server.""" + self._send_notification("notebookDocument/didClose", params=params) + def text_document_formatting(self, formatting_params): """Sends text document format request to LSP server.""" fut = self._send_request("textDocument/formatting", params=formatting_params) diff --git a/src/test/python_tests/test_data/sample1/sample.ipynb b/src/test/python_tests/test_data/sample1/sample.ipynb new file mode 100644 index 00000000..1a4e7305 --- /dev/null +++ b/src/test/python_tests/test_data/sample1/sample.ipynb @@ -0,0 +1,27 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "cell1", + "metadata": {}, + "outputs": [], + "source": [ + "x = 1\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/test/python_tests/test_notebook.py b/src/test/python_tests/test_notebook.py new file mode 100644 index 00000000..5ae47fe0 --- /dev/null +++ b/src/test/python_tests/test_notebook.py @@ -0,0 +1,426 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +""" +Tests for Jupyter notebook cell support over LSP. + +These are template-style example tests that demonstrate how to validate notebook +cell diagnostics. Adapt the expected diagnostics to match your tool's output. +""" + +import os +from threading import Event + +from .lsp_test_client import constants, defaults, session, utils + +TIMEOUT = 10 # seconds + + +def _make_notebook_uri(notebook_path: str) -> str: + """Returns a 'file:' URI for a notebook path.""" + return utils.as_uri(notebook_path) + + +def _make_cell_uri(notebook_path: str, cell_id: str) -> str: + """Returns a 'vscode-notebook-cell:' URI for a notebook cell. + + Args: + notebook_path: Absolute path to the .ipynb file. + cell_id: Fragment identifier for the cell (e.g. 'W0sZmlsZQ%3D%3D0'). + """ + nb_uri = utils.as_uri(notebook_path) + # Replace 'file:' scheme with 'vscode-notebook-cell:' + cell_uri = nb_uri.replace("file:", "vscode-notebook-cell:", 1) + return f"{cell_uri}#{cell_id}" + + +def test_notebook_did_open(): + """Diagnostics are published for each code cell when a notebook is opened. + + This test sends a notebookDocument/didOpen notification for a notebook with + one code cell and verifies that a publishDiagnostics notification is received + for that cell's URI. + + TODO: Update the expected diagnostics to match your tool's output. + """ + nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") + nb_uri = _make_notebook_uri(nb_path) + cell_id = "cell1" + cell_uri = _make_cell_uri(nb_path, cell_id) + cell_contents = "x = 1\n" + + with session.LspSession() as ls_session: + ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + + done = Event() + received = [] + + def _handler(params): + received.append(params) + done.set() + + ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) + + ls_session.notify_notebook_did_open( + { + "notebookDocument": { + "uri": nb_uri, + "notebookType": "jupyter-notebook", + "version": 1, + "metadata": {}, + "cells": [ + { + "kind": 2, # Code cell + "document": cell_uri, + "metadata": {}, + "executionSummary": None, + } + ], + }, + "cellTextDocuments": [ + { + "uri": cell_uri, + "languageId": "python", + "version": 1, + "text": cell_contents, + } + ], + } + ) + + done.wait(TIMEOUT) + + # TODO: Add your tool-specific assertion on `received`. + # For now, just verify we got a diagnostics notification for the cell. + assert any( + r.get("uri") == cell_uri for r in received + ), f"Expected diagnostics for {cell_uri!r}, got: {received}" + + +def test_notebook_did_change_text_content(): + """Diagnostics update when the text content of a cell changes. + + TODO: Update the expected diagnostics to match your tool's output. + """ + nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") + nb_uri = _make_notebook_uri(nb_path) + cell_id = "cell1" + cell_uri = _make_cell_uri(nb_path, cell_id) + initial_contents = "x = 1\n" + updated_contents = "y = 2\n" + + with session.LspSession() as ls_session: + ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + + # Open notebook first + ls_session.notify_notebook_did_open( + { + "notebookDocument": { + "uri": nb_uri, + "notebookType": "jupyter-notebook", + "version": 1, + "metadata": {}, + "cells": [ + { + "kind": 2, + "document": cell_uri, + "metadata": {}, + "executionSummary": None, + } + ], + }, + "cellTextDocuments": [ + { + "uri": cell_uri, + "languageId": "python", + "version": 1, + "text": initial_contents, + } + ], + } + ) + + done = Event() + received = [] + + def _handler(params): + received.append(params) + done.set() + + ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) + + # Send a change with updated text content + ls_session.notify_notebook_did_change( + { + "notebookDocument": { + "uri": nb_uri, + "version": 2, + }, + "change": { + "metadata": None, + "cells": { + "structure": None, + "data": None, + "textContent": [ + { + "document": {"uri": cell_uri, "version": 2}, + "changes": [ + { + "text": updated_contents, + } + ], + } + ], + }, + }, + } + ) + + done.wait(TIMEOUT) + + # TODO: Add your tool-specific assertion on `received`. + assert any( + r.get("uri") == cell_uri for r in received + ), f"Expected diagnostics for {cell_uri!r}, got: {received}" + + +def test_notebook_did_save(): + """All code cells are re-linted when a notebook is saved. + + TODO: Update the expected diagnostics to match your tool's output. + """ + nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") + nb_uri = _make_notebook_uri(nb_path) + cell_id = "cell1" + cell_uri = _make_cell_uri(nb_path, cell_id) + cell_contents = "x = 1\n" + + with session.LspSession() as ls_session: + ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + + # Open notebook first + ls_session.notify_notebook_did_open( + { + "notebookDocument": { + "uri": nb_uri, + "notebookType": "jupyter-notebook", + "version": 1, + "metadata": {}, + "cells": [ + { + "kind": 2, + "document": cell_uri, + "metadata": {}, + "executionSummary": None, + } + ], + }, + "cellTextDocuments": [ + { + "uri": cell_uri, + "languageId": "python", + "version": 1, + "text": cell_contents, + } + ], + } + ) + + done = Event() + received = [] + + def _handler(params): + received.append(params) + done.set() + + ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) + + ls_session.notify_notebook_did_save( + { + "notebookDocument": { + "uri": nb_uri, + "version": 1, + } + } + ) + + done.wait(TIMEOUT) + + # TODO: Add your tool-specific assertion on `received`. + assert any( + r.get("uri") == cell_uri for r in received + ), f"Expected diagnostics for {cell_uri!r}, got: {received}" + + +def test_notebook_did_change_new_cell_kind_filter(): + """Diagnostics are only published for newly added code cells, not markdown cells. + + When a notebook change adds both a code cell and a markdown cell via + structure.did_open, only the code cell should receive diagnostics. + """ + nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") + nb_uri = _make_notebook_uri(nb_path) + code_cell_id = "cell_code" + md_cell_id = "cell_md" + code_cell_uri = _make_cell_uri(nb_path, code_cell_id) + md_cell_uri = _make_cell_uri(nb_path, md_cell_id) + + with session.LspSession() as ls_session: + ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + + # Open an initially empty notebook + ls_session.notify_notebook_did_open( + { + "notebookDocument": { + "uri": nb_uri, + "notebookType": "jupyter-notebook", + "version": 1, + "metadata": {}, + "cells": [], + }, + "cellTextDocuments": [], + } + ) + + received = [] + done = Event() + + def _handler(params): + received.append(params) + done.set() + + ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) + + # Add both a code cell (kind=2) and a markdown cell (kind=1) at once + ls_session.notify_notebook_did_change( + { + "notebookDocument": { + "uri": nb_uri, + "version": 2, + }, + "change": { + "metadata": None, + "cells": { + "structure": { + "array": { + "start": 0, + "deleteCount": 0, + "cells": [ + { + "kind": 2, # Code + "document": code_cell_uri, + "metadata": {}, + "executionSummary": None, + }, + { + "kind": 1, # Markdown + "document": md_cell_uri, + "metadata": {}, + "executionSummary": None, + }, + ], + }, + "didOpen": [ + { + "uri": code_cell_uri, + "languageId": "python", + "version": 1, + "text": "x = 1\n", + }, + { + "uri": md_cell_uri, + "languageId": "markdown", + "version": 1, + "text": "# heading\n", + }, + ], + "didClose": None, + }, + "data": None, + "textContent": None, + }, + }, + } + ) + + done.wait(TIMEOUT) + + # The code cell should receive diagnostics; the markdown cell must not. + uris_with_diagnostics = {r.get("uri") for r in received} + assert code_cell_uri in uris_with_diagnostics, ( + f"Expected diagnostics for code cell {code_cell_uri!r}, got: {received}" + ) + assert md_cell_uri not in uris_with_diagnostics, ( + f"Markdown cell {md_cell_uri!r} should not receive diagnostics, got: {received}" + ) + + +def test_notebook_did_close(): + """Diagnostics are cleared for all cells when a notebook is closed. + + TODO: Update the expected diagnostics to match your tool's output. + """ + nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") + nb_uri = _make_notebook_uri(nb_path) + cell_id = "cell1" + cell_uri = _make_cell_uri(nb_path, cell_id) + cell_contents = "x = 1\n" + + with session.LspSession() as ls_session: + ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + + # Open notebook first + ls_session.notify_notebook_did_open( + { + "notebookDocument": { + "uri": nb_uri, + "notebookType": "jupyter-notebook", + "version": 1, + "metadata": {}, + "cells": [ + { + "kind": 2, + "document": cell_uri, + "metadata": {}, + "executionSummary": None, + } + ], + }, + "cellTextDocuments": [ + { + "uri": cell_uri, + "languageId": "python", + "version": 1, + "text": cell_contents, + } + ], + } + ) + + done = Event() + received = [] + + def _handler(params): + received.append(params) + done.set() + + ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) + + ls_session.notify_notebook_did_close( + { + "notebookDocument": { + "uri": nb_uri, + "version": 1, + }, + "cellTextDocuments": [ + {"uri": cell_uri} + ], + } + ) + + done.wait(TIMEOUT) + + # Diagnostics should be cleared (empty list) for the cell URI + assert any( + r.get("uri") == cell_uri and r.get("diagnostics") == [] + for r in received + ), f"Expected empty diagnostics for {cell_uri!r}, got: {received}" From 96bf9c6befcb75c0be7559fe2d8e09fb0c1d2471 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Mar 2026 14:49:27 -0700 Subject: [PATCH 5/9] Fix test_notebook.py: use vscode_initialize_defaults() function The repos use defaults.vscode_initialize_defaults() (a function), not defaults.VSCODE_DEFAULT_INITIALIZE (a constant). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/test/python_tests/test_notebook.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/python_tests/test_notebook.py b/src/test/python_tests/test_notebook.py index 5ae47fe0..48f3b487 100644 --- a/src/test/python_tests/test_notebook.py +++ b/src/test/python_tests/test_notebook.py @@ -49,7 +49,7 @@ def test_notebook_did_open(): cell_contents = "x = 1\n" with session.LspSession() as ls_session: - ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + ls_session.initialize(defaults.vscode_initialize_defaults()) done = Event() received = [] @@ -109,7 +109,7 @@ def test_notebook_did_change_text_content(): updated_contents = "y = 2\n" with session.LspSession() as ls_session: - ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + ls_session.initialize(defaults.vscode_initialize_defaults()) # Open notebook first ls_session.notify_notebook_did_open( @@ -195,7 +195,7 @@ def test_notebook_did_save(): cell_contents = "x = 1\n" with session.LspSession() as ls_session: - ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + ls_session.initialize(defaults.vscode_initialize_defaults()) # Open notebook first ls_session.notify_notebook_did_open( @@ -265,7 +265,7 @@ def test_notebook_did_change_new_cell_kind_filter(): md_cell_uri = _make_cell_uri(nb_path, md_cell_id) with session.LspSession() as ls_session: - ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + ls_session.initialize(defaults.vscode_initialize_defaults()) # Open an initially empty notebook ls_session.notify_notebook_did_open( @@ -366,7 +366,7 @@ def test_notebook_did_close(): cell_contents = "x = 1\n" with session.LspSession() as ls_session: - ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) + ls_session.initialize(defaults.vscode_initialize_defaults()) # Open notebook first ls_session.notify_notebook_did_open( From bdc66903afe520f00fb91c79620f5b074ec3c8ac Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Mar 2026 14:57:21 -0700 Subject: [PATCH 6/9] Fix test_notebook_did_close race condition Drain didOpen diagnostics before registering the close callback to avoid capturing stale open-diagnostics instead of the expected empty close-diagnostics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/test/python_tests/test_notebook.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/test/python_tests/test_notebook.py b/src/test/python_tests/test_notebook.py index 48f3b487..99304a76 100644 --- a/src/test/python_tests/test_notebook.py +++ b/src/test/python_tests/test_notebook.py @@ -368,7 +368,16 @@ def test_notebook_did_close(): with session.LspSession() as ls_session: ls_session.initialize(defaults.vscode_initialize_defaults()) - # Open notebook first + # Open notebook and wait for the initial diagnostics to arrive + open_done = Event() + + def _open_handler(params): + open_done.set() + + ls_session.set_notification_callback( + session.PUBLISH_DIAGNOSTICS, _open_handler + ) + ls_session.notify_notebook_did_open( { "notebookDocument": { @@ -396,6 +405,9 @@ def test_notebook_did_close(): } ) + open_done.wait(TIMEOUT) + + # Now set up a fresh callback for the close notification done = Event() received = [] From 80dcc7af162b4d3decb292d3911b0f0df8542744 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Mar 2026 15:01:46 -0700 Subject: [PATCH 7/9] Fix lint: remove unused os import, TODO comments, unused param Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/test/python_tests/test_notebook.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/test/python_tests/test_notebook.py b/src/test/python_tests/test_notebook.py index 99304a76..fec15572 100644 --- a/src/test/python_tests/test_notebook.py +++ b/src/test/python_tests/test_notebook.py @@ -7,7 +7,6 @@ cell diagnostics. Adapt the expected diagnostics to match your tool's output. """ -import os from threading import Event from .lsp_test_client import constants, defaults, session, utils @@ -39,8 +38,6 @@ def test_notebook_did_open(): This test sends a notebookDocument/didOpen notification for a notebook with one code cell and verifies that a publishDiagnostics notification is received for that cell's URI. - - TODO: Update the expected diagnostics to match your tool's output. """ nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") nb_uri = _make_notebook_uri(nb_path) @@ -88,8 +85,6 @@ def _handler(params): ) done.wait(TIMEOUT) - - # TODO: Add your tool-specific assertion on `received`. # For now, just verify we got a diagnostics notification for the cell. assert any( r.get("uri") == cell_uri for r in received @@ -98,8 +93,6 @@ def _handler(params): def test_notebook_did_change_text_content(): """Diagnostics update when the text content of a cell changes. - - TODO: Update the expected diagnostics to match your tool's output. """ nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") nb_uri = _make_notebook_uri(nb_path) @@ -176,8 +169,6 @@ def _handler(params): ) done.wait(TIMEOUT) - - # TODO: Add your tool-specific assertion on `received`. assert any( r.get("uri") == cell_uri for r in received ), f"Expected diagnostics for {cell_uri!r}, got: {received}" @@ -185,8 +176,6 @@ def _handler(params): def test_notebook_did_save(): """All code cells are re-linted when a notebook is saved. - - TODO: Update the expected diagnostics to match your tool's output. """ nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") nb_uri = _make_notebook_uri(nb_path) @@ -244,8 +233,6 @@ def _handler(params): ) done.wait(TIMEOUT) - - # TODO: Add your tool-specific assertion on `received`. assert any( r.get("uri") == cell_uri for r in received ), f"Expected diagnostics for {cell_uri!r}, got: {received}" @@ -356,8 +343,6 @@ def _handler(params): def test_notebook_did_close(): """Diagnostics are cleared for all cells when a notebook is closed. - - TODO: Update the expected diagnostics to match your tool's output. """ nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") nb_uri = _make_notebook_uri(nb_path) @@ -371,7 +356,7 @@ def test_notebook_did_close(): # Open notebook and wait for the initial diagnostics to arrive open_done = Event() - def _open_handler(params): + def _open_handler(_params): open_done.set() ls_session.set_notification_callback( From 89ff5633b9f72203a40394c079709fe8839993b2 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Mar 2026 16:25:37 -0700 Subject: [PATCH 8/9] Run black formatter on changed files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bundled/tool/lsp_server.py | 7 +++++- src/test/python_tests/test_notebook.py | 32 ++++++++++---------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/bundled/tool/lsp_server.py b/bundled/tool/lsp_server.py index 0eda192a..b0ac2c8e 100644 --- a/bundled/tool/lsp_server.py +++ b/bundled/tool/lsp_server.py @@ -93,7 +93,12 @@ def update_environ_path() -> None: ], save=True, ) -LSP_SERVER = LanguageServer(name="Mypy", version="v0.1.0", max_workers=MAX_WORKERS, notebook_document_sync=NOTEBOOK_SYNC_OPTIONS) +LSP_SERVER = LanguageServer( + name="Mypy", + version="v0.1.0", + max_workers=MAX_WORKERS, + notebook_document_sync=NOTEBOOK_SYNC_OPTIONS, +) DMYPY_ARGS = {} DMYPY_STATUS_FILE_ROOT = None diff --git a/src/test/python_tests/test_notebook.py b/src/test/python_tests/test_notebook.py index fec15572..e04162ad 100644 --- a/src/test/python_tests/test_notebook.py +++ b/src/test/python_tests/test_notebook.py @@ -92,8 +92,7 @@ def _handler(params): def test_notebook_did_change_text_content(): - """Diagnostics update when the text content of a cell changes. - """ + """Diagnostics update when the text content of a cell changes.""" nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") nb_uri = _make_notebook_uri(nb_path) cell_id = "cell1" @@ -175,8 +174,7 @@ def _handler(params): def test_notebook_did_save(): - """All code cells are re-linted when a notebook is saved. - """ + """All code cells are re-linted when a notebook is saved.""" nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") nb_uri = _make_notebook_uri(nb_path) cell_id = "cell1" @@ -333,17 +331,16 @@ def _handler(params): # The code cell should receive diagnostics; the markdown cell must not. uris_with_diagnostics = {r.get("uri") for r in received} - assert code_cell_uri in uris_with_diagnostics, ( - f"Expected diagnostics for code cell {code_cell_uri!r}, got: {received}" - ) - assert md_cell_uri not in uris_with_diagnostics, ( - f"Markdown cell {md_cell_uri!r} should not receive diagnostics, got: {received}" - ) + assert ( + code_cell_uri in uris_with_diagnostics + ), f"Expected diagnostics for code cell {code_cell_uri!r}, got: {received}" + assert ( + md_cell_uri not in uris_with_diagnostics + ), f"Markdown cell {md_cell_uri!r} should not receive diagnostics, got: {received}" def test_notebook_did_close(): - """Diagnostics are cleared for all cells when a notebook is closed. - """ + """Diagnostics are cleared for all cells when a notebook is closed.""" nb_path = str(constants.TEST_DATA / "sample1" / "sample.ipynb") nb_uri = _make_notebook_uri(nb_path) cell_id = "cell1" @@ -359,9 +356,7 @@ def test_notebook_did_close(): def _open_handler(_params): open_done.set() - ls_session.set_notification_callback( - session.PUBLISH_DIAGNOSTICS, _open_handler - ) + ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _open_handler) ls_session.notify_notebook_did_open( { @@ -408,9 +403,7 @@ def _handler(params): "uri": nb_uri, "version": 1, }, - "cellTextDocuments": [ - {"uri": cell_uri} - ], + "cellTextDocuments": [{"uri": cell_uri}], } ) @@ -418,6 +411,5 @@ def _handler(params): # Diagnostics should be cleared (empty list) for the cell URI assert any( - r.get("uri") == cell_uri and r.get("diagnostics") == [] - for r in received + r.get("uri") == cell_uri and r.get("diagnostics") == [] for r in received ), f"Expected empty diagnostics for {cell_uri!r}, got: {received}" From 8546df9b4905701a2a633065b2b6180c6b830872 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 10 Mar 2026 16:51:39 -0700 Subject: [PATCH 9/9] Filter non-Python cells in notebook_did_change handler Skip cells with languageId != 'python' in structure.did_open to avoid running the linter on markdown or other non-Python cells. Also clear diagnostics on exception in mypy's _linting_helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bundled/tool/lsp_server.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bundled/tool/lsp_server.py b/bundled/tool/lsp_server.py index b0ac2c8e..92f42bce 100644 --- a/bundled/tool/lsp_server.py +++ b/bundled/tool/lsp_server.py @@ -250,6 +250,8 @@ def notebook_did_change(params: lsp.DidChangeNotebookDocumentParams) -> None: structure = params.change.cells.structure if structure and structure.did_open: for cell_doc in structure.did_open: + if cell_doc.language_id != "python": + continue document = LSP_SERVER.workspace.get_text_document(cell_doc.uri) _linting_helper(document) @@ -385,6 +387,7 @@ def _linting_helper(document: TextDocument) -> None: message=f"Linting failed with error:\r\n{traceback.format_exc()}", ) ) + _clear_diagnostics(document) return []