Skip to content
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

lsp-devtools: Retry connection indefinitely #77

Merged
merged 3 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lsp-devtools-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
os: [ubuntu-latest]

steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.coverage
.env
.tox
*.pyc
Expand Down
1 change: 1 addition & 0 deletions lib/lsp-devtools/changes/77.enhancement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
If the agent is unable to connect to a server app immediately, it will now retry indefinitely until it succeeds or the language server exits
1 change: 1 addition & 0 deletions lib/lsp-devtools/changes/77.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Drop Python 3.7 support
29 changes: 20 additions & 9 deletions lib/lsp-devtools/lsp_devtools/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,26 @@ def __init__(self, client: AgentClient, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = client
self.session = str(uuid4())
self._buffer: List[MessageText] = []

def emit(self, record: logging.LogRecord):
self.client.protocol.message_text_notification(
MessageText(
text=record.args[0], # type: ignore
session=self.session,
timestamp=record.created,
source=record.__dict__["source"],
)
message = MessageText(
text=record.args[0], # type: ignore
session=self.session,
timestamp=record.created,
source=record.__dict__["source"],
)

if not self.client.connected:
self._buffer.append(message)
return

# Send any buffered messages
while len(self._buffer) > 0:
self.client.protocol.message_text_notification(self._buffer.pop(0))

self.client.protocol.message_text_notification(message)


async def main(args, extra: List[str]):
if extra is None:
Expand All @@ -68,8 +77,10 @@ async def main(args, extra: List[str]):
logger.setLevel(logging.INFO)
logger.addHandler(handler)

await client.start_tcp(args.host, args.port)
await agent.start()
await asyncio.gather(
client.start_tcp(args.host, args.port),
agent.start(),
)


def run_agent(args, extra: List[str]):
Expand Down
45 changes: 30 additions & 15 deletions lib/lsp-devtools/lsp_devtools/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,31 @@
from typing import Any
from typing import Optional

import stamina
from pygls.client import Client
from pygls.client import aio_readline
from pygls.protocol import default_converter
from websockets.client import WebSocketClientProtocol

from lsp_devtools.agent.protocol import AgentProtocol

# from websockets.client import WebSocketClientProtocol

class WebSocketClientTransportAdapter:
"""Protocol adapter for the WebSocket client interface."""

def __init__(self, ws: WebSocketClientProtocol, loop: asyncio.AbstractEventLoop):
self._ws = ws
self._loop = loop
# class WebSocketClientTransportAdapter:
# """Protocol adapter for the WebSocket client interface."""

def close(self) -> None:
"""Stop the WebSocket server."""
print("-- CLOSING --")
self._loop.create_task(self._ws.close())
# def __init__(self, ws: WebSocketClientProtocol, loop: asyncio.AbstractEventLoop):
# self._ws = ws
# self._loop = loop

def write(self, data: Any) -> None:
"""Create a task to write specified data into a WebSocket."""
asyncio.ensure_future(self._ws.send(data))
# def close(self) -> None:
# """Stop the WebSocket server."""
# print("-- CLOSING --")
# self._loop.create_task(self._ws.close())

# def write(self, data: Any) -> None:
# """Create a task to write specified data into a WebSocket."""
# asyncio.ensure_future(self._ws.send(data))


class AgentClient(Client):
Expand All @@ -36,6 +38,7 @@ def __init__(self):
super().__init__(
protocol_cls=AgentProtocol, converter_factory=default_converter
)
self.connected = False

def _report_server_error(self, error, source):
# Bail on error
Expand All @@ -47,13 +50,25 @@ def feature(self, feature_name: str, options: Optional[Any] = None):

# TODO: Upstream this... or at least something equivalent.
async def start_tcp(self, host: str, port: int):
reader, writer = await asyncio.open_connection(host, port)
# The user might not have started the server app immediately and since the
# agent will live as long as the wrapper language server we may as well
# try indefinitely.
retries = stamina.retry_context(
on=OSError,
attempts=None,
timeout=None,
wait_initial=1,
wait_max=60,
)
async for attempt in retries:
with attempt:
reader, writer = await asyncio.open_connection(host, port)

# adapter = TCPTransportAdapter(writer)
self.protocol.connection_made(writer)
connection = asyncio.create_task(
aio_readline(self._stop_event, reader, self.protocol.data_received)
)
self.connected = True
self._async_tasks.append(connection)

# TODO: Upstream this... or at least something equivalent.
Expand Down
7 changes: 1 addition & 6 deletions lib/lsp-devtools/lsp_devtools/handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import json
import logging
from typing import Any
from typing import Literal
from typing import Mapping
from typing import Optional
from uuid import uuid4

import attrs

try:
from typing import Literal
except ImportError:
from typing_extensions import Literal # type: ignore[assignment]


MessageSource = Literal["client", "server"]


Expand Down
7 changes: 1 addition & 6 deletions lib/lsp-devtools/lsp_devtools/record/filters.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import logging
from typing import Dict
from typing import Literal
from typing import Set
from typing import Union

import attrs

from .formatters import FormatString

try:
from typing import Literal
except ImportError:
from typing_extensions import Literal # type: ignore[assignment]


logger = logging.getLogger(__name__)

MessageSource = Literal["client", "server", "both"]
Expand Down
2 changes: 1 addition & 1 deletion lib/lsp-devtools/lsp_devtools/record/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class FormatString:
VARIABLE = re.compile(r"{\.([^}]+)}")

def __init__(self, pattern: str):
self.pattern = pattern.replace("\\n", "\n").replace("\\t", "\t")
self.pattern = pattern # .replace("\\n", "\n").replace("\\t", "\t")
self._parse()

def _parse(self):
Expand Down
30 changes: 10 additions & 20 deletions lib/lsp-devtools/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "lsp-devtools"
version = "0.1.1"
description = "Developer tooling for language servers"
readme = "README.md"
requires-python = ">=3.7"
requires-python = ">=3.8"
license = { text = "MIT" }
authors = [{ name = "Alex Carney", email = "alcarneyme@gmail.com" }]
classifiers = [
Expand All @@ -16,7 +16,6 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand All @@ -27,8 +26,8 @@ dependencies = [
"aiosqlite",
"importlib-resources; python_version<\"3.9\"",
"pygls",
"stamina",
"textual>=0.14.0",
"typing-extensions; python_version<\"3.8\"",
]

[project.urls]
Expand All @@ -43,10 +42,6 @@ dev = [
"pre-commit",
"tox",
]
test=[
"pytest-cov",
"pytest-timeout",
]
typecheck=[
"mypy",
"importlib_resources",
Expand All @@ -61,6 +56,14 @@ lsp-devtools = "lsp_devtools.cli:main"
[tool.setuptools.packages.find]
include = ["lsp_devtools*"]

[tool.coverage.run]
source_pkgs = ["lsp_devtools"]

[tool.coverage.report]
show_missing = true
skip_covered = true
sort = "Cover"

[tool.isort]
force_single_line = true
profile = "black"
Expand Down Expand Up @@ -101,16 +104,3 @@ showcontent = true
directory = "misc"
name = "Misc"
showcontent = true

[tool.tox]
legacy_tox_ini = """
[tox]
isolated_build = True
skip_missing_interpreters = true
envlist = py{37,38,39,310,311}

[testenv]
extras=test
commands =
pytest {posargs}
"""
8 changes: 4 additions & 4 deletions lib/lsp-devtools/tests/record/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"textDocument": {"uri": "file:///path/to/file.txt"},
},
},
"textDocument/completion file:///path/to/file.txt:{'line': 1, 'character': 2}", # noqa: E501
'textDocument/completion file:///path/to/file.txt:{\n "line": 1,\n "character": 2\n}', # noqa: E501
),
(
"{.method} {.params.textDocument.uri}:{.params.position|Position}",
Expand Down Expand Up @@ -61,7 +61,7 @@
"items": [{"label": "one"}, {"label": "two"}, {"label": "three"}]
}
},
"{'label': 'one'}\n{'label': 'two'}\n{'label': 'three'}",
'{\n "label": "one"\n}\n{\n "label": "two"\n}\n{\n "label": "three"\n}',
),
(
"{.result.items[].label}",
Expand All @@ -88,7 +88,7 @@
"items": [{"label": "one"}, {"label": "two"}, {"label": "three"}]
}
},
"{'label': 'one'}",
'{\n "label": "one"\n}',
),
(
"{.result.items[-1]}",
Expand All @@ -97,7 +97,7 @@
"items": [{"label": "one"}, {"label": "two"}, {"label": "three"}]
}
},
"{'label': 'three'}",
'{\n "label": "three"\n}',
),
(
"- {.result.items[0].label}",
Expand Down
21 changes: 21 additions & 0 deletions lib/lsp-devtools/tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[tox]
isolated_build = True
skip_missing_interpreters = true
min_version = 4.0
envlist = py{38,39,310,311}

[testenv]
description = "Run lsp-devtools' test suite"
package = wheel
wheel_build_env = .pkg
deps =
coverage[toml]
pytest

git+https://github.com/openlawlibrary/pygls
commands_pre =
coverage erase
commands =
coverage run -m pytest {posargs}
commands_post =
coverage report