-
Notifications
You must be signed in to change notification settings - Fork 133
[wip] refactor python language server #10908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
E2E Tests 🚀 |
e253979 to
82a7c52
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR refactors the Python language server implementation by replacing the jedi-language-server dependency with a custom lightweight LSP server implementation. The new server maintains Positron-specific features (namespace-aware completions, DataFrame/Series column completions, magic commands, help topics, syntax diagnostics) while delegating static analysis features to third-party extensions like Pylance.
Key changes:
- Removed jedi-language-server, jedi, and parso dependencies and vendored code
- Created new
positron_lsp.pyimplementing a minimal LSP server using pygls directly - Removed complex Jedi patches and integration code
- Updated dependency versions (pygls 2.0.0, lsprotocol 2025.0.0)
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
scripts/patches/*.patch |
Removed patch files for jedi, parso, and jedi-language-server |
python_files/run-jedi-language-server.py |
Added type ignore comment for import |
python_files/positron_requirements/requirements.txt |
Updated dependencies, removed jedi-language-server, jedi, parso |
python_files/positron_requirements/requirements.in |
Removed jedi-language-server, upgraded pygls to 2.0.0 |
python_files/posit/positron_language_server.py |
Updated import from positron_jedilsp to positron_lsp |
python_files/posit/positron/tests/test_positron_lsp.py |
New test file for the refactored LSP server |
python_files/posit/positron/tests/test_positron_jedilsp.py |
Removed old test file |
python_files/posit/positron/positron_lsp.py |
New LSP server implementation |
python_files/posit/positron/positron_jedilsp.py |
Removed old LSP server implementation |
python_files/posit/positron/lsp.py |
Updated import from positron_jedilsp to positron_lsp |
python_files/posit/positron/jedi.py |
Removed Jedi patches and integration code |
| elif inspect.ismethod(obj) or inspect.isfunction(obj): | ||
| return types.CompletionItemKind.Function |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lines 782 and 784 return the same value. The elif branch checking for methods/functions is redundant since the else branch will also return CompletionItemKind.Function. Consider simplifying by removing the elif or add a comment explaining why this distinction exists in the code structure.
| elif inspect.ismethod(obj) or inspect.isfunction(obj): | |
| return types.CompletionItemKind.Function |
| # Fall back to the expression itself | ||
| topic = expr | ||
|
|
||
| logger.info(f"Help topic found: {topic}") |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using f-string formatting in logging calls can cause performance issues since the string is always formatted even if the logging level would skip it. Consider using lazy formatting: logger.info('Help topic found: %s', topic)
| logger.info(f"Help topic found: {topic}") | |
| logger.info("Help topic found: %s", topic) |
| try: | ||
| _publish_diagnostics(server, uri) | ||
| except Exception: | ||
| logger.exception(f"Failed to publish diagnostics for {uri}", exc_info=True) |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logger.exception() already includes exc_info=True by default, so passing it explicitly is redundant. Consider removing the exc_info=True parameter.
| logger.exception(f"Failed to publish diagnostics for {uri}", exc_info=True) | |
| logger.exception(f"Failed to publish diagnostics for {uri}") |
seeM
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a look at the code and it looks like a great start!
|
|
||
|
|
||
| from jedi_language_server.cli import cli # noqa: E402 | ||
| from jedi_language_server.cli import cli # type: ignore # noqa: E402 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this an upstream file? If so can we ignore in the pyproject.toml instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I could add this to our pyproject.toml!
| self._messages_to_handle: list[Any] = [] | ||
|
|
||
| @lru_cache # noqa: B019 | ||
| def get_message_type(self, method: str) -> type | None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this pygls' recommended way to add methods? I wonder if there's something neater now that we're not depending on JediLanguageServerProtocol
| init_opts = PositronInitializationOptions() | ||
|
|
||
| # Store the working directory (using params.root_path since workspace may not be initialized yet) | ||
| server._working_directory = init_opts.working_directory or params.root_path # noqa: SLF001 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also wonder if there's another pygls recommended way
| # Yield to parent implementation which handles workspace setup | ||
| return (yield from super().lsp_initialize(params)) | ||
|
|
||
| def _data_received(self, data: bytes) -> None: # type: ignore[override] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need this anymore, and may want to try to make our server async
| # Cache for magic completions | ||
| self._magic_completions: dict[str, tuple] = {} | ||
|
|
||
| def start_tcp(self, host: str) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible we don't need all of this in the latest pygls. A lot of this was because pygls didn't let you use your own loop, which we needed to do
| elif trimmed_line.startswith((_LINE_MAGIC_PREFIX, _CELL_MAGIC_PREFIX)): | ||
| # Magic command completion only | ||
| pass # Will add magics below | ||
| else: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't checked, are we providing the right completions (if any) inside string literals? Do we provide path completions?
|
|
||
| # Try to evaluate the expression | ||
| try: | ||
| obj = eval(expr, server.shell.user_ns) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like a really dangerous thing to do!
| # Get keys based on the type of object | ||
| if isinstance(obj, dict): | ||
| keys = [str(k) for k in obj if isinstance(k, str)] | ||
| elif _is_environ_like(obj): | ||
| # os.environ or similar | ||
| keys = list(os.environ.keys()) | ||
| elif _is_dataframe_like(obj): | ||
| # pandas/polars DataFrame | ||
| with contextlib.suppress(Exception): | ||
| keys = list(obj.columns) | ||
| elif _is_series_like(obj): | ||
| # pandas Series with string index | ||
| with contextlib.suppress(Exception): | ||
| keys = [str(k) for k in obj.index if isinstance(k, str)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure yet if it's a good idea, but we could consider using the existing inspectors code to abstract this sort of thing
|
|
||
| # Try to evaluate the expression in the namespace | ||
| try: | ||
| obj = eval(expr, server.shell.user_ns) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do other libs handle safe evaluation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like parso doesn't eval code at all. I'll see if I can adopt a similar strategy (or just use parso)
| logger.exception(f"Failed to publish diagnostics for {uri}", exc_info=True) | ||
|
|
||
|
|
||
| def _publish_diagnostics(server: PositronLanguageServer, uri: str) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this only run in console?
Release Notes
New Features
Bug Fixes
QA Notes
@:console @:help @:notebooks @:positron-notebooks @:outline @:problems @:references @:web @:win