Skip to content

Commit 57c83ca

Browse files
Merge pull request #4 from promptmesh/feat/testing
Setup pytest
2 parents b6615fc + 30e4e1d commit 57c83ca

21 files changed

+449
-211
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ wheels/
1010
.venv
1111

1212
# repomix
13-
repomix-output.xml
13+
repomix-output.xml
14+
15+
# pytest
16+
.coverage

.vscode/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"python.testing.pytestArgs": [
3+
"tests"
4+
],
5+
"python.testing.unittestEnabled": false,
6+
"python.testing.pytestEnabled": true
7+
}

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,12 @@ build-backend = "hatchling.build"
2525
[dependency-groups]
2626
dev = [
2727
"mypy>=1.15.0",
28+
"pytest>=8.3.5",
29+
"pytest-asyncio>=0.26.0",
30+
"pytest-cov>=6.1.0",
31+
"pytest-rerunfailures>=15.0",
2832
]
33+
34+
[tool.pytest.ini_options]
35+
asyncio_mode = "strict"
36+
asyncio_default_fixture_loop_scope = "function"

src/easymcp/client/iobuffers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from asyncio import Queue, Task
1+
from asyncio import Queue, create_task
22

33
from loguru import logger
44
import pydantic
@@ -24,7 +24,7 @@ async def _reader():
2424

2525
queue.put_nowait(parsed)
2626

27-
task = Task(_reader())
27+
task = create_task(_reader())
2828
return task
2929

3030

@@ -36,5 +36,5 @@ async def _writer():
3636
data = await queue.get()
3737
await transport.send(data.model_dump_json())
3838

39-
task = Task(_writer())
39+
task = create_task(_writer())
4040
return task

src/easymcp/client/sessions/mcp/session.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from asyncio import Queue, Task
1+
from asyncio import Queue, Task, create_task
2+
import asyncio
23
from inspect import iscoroutinefunction
34
import json
45
from typing import Awaitable, Callable
@@ -23,6 +24,8 @@ class MCPClientSession(BaseSessionProtocol):
2324
reader_task: Task[None]
2425
writer_task: Task[None]
2526

27+
_start_reading_messages_task: Task[None]
28+
2629
request_map: RequestMap
2730

2831
roots_callback: Callable[[types.ListRootsRequest], Awaitable[types.ListRootsResult]] | None = None
@@ -136,7 +139,7 @@ async def __start_reading_messages():
136139
else:
137140
logger.error(f"Unknown message type: {message.root}")
138141

139-
Task(__start_reading_messages())
142+
self._start_reading_messages_task = create_task(__start_reading_messages())
140143

141144
async def start(self) -> types.InitializeResult:
142145
"""start the client session"""
@@ -188,7 +191,26 @@ async def start(self) -> types.InitializeResult:
188191

189192
async def stop(self):
190193
"""stop the client session"""
194+
self.reader_task.cancel()
195+
self.writer_task.cancel()
196+
try:
197+
await self.reader_task
198+
except asyncio.CancelledError:
199+
pass
200+
201+
try:
202+
self.writer_task
203+
except asyncio.CancelledError:
204+
pass
205+
206+
self._start_reading_messages_task.cancel()
207+
try:
208+
await self._start_reading_messages_task
209+
except asyncio.CancelledError:
210+
pass
211+
191212
await self.transport.stop()
213+
await asyncio.sleep(0)
192214

193215
async def list_tools(self, force: bool = False):
194216
"""list available tools"""

src/easymcp/client/transports/docker.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Literal
1+
import asyncio
2+
from typing import Any, Literal
23

34
import anyio
45
import anyio.abc
@@ -7,6 +8,8 @@
78
from loguru import logger
89
from pydantic import BaseModel, Field
910

11+
from easymcp.client.transports.generic import TransportProtocol
12+
1013

1114
class DockerServerParameters(BaseModel):
1215
"""Configuration for Docker transport."""
@@ -22,14 +25,14 @@ class DockerServerParameters(BaseModel):
2225
)
2326

2427

25-
class DockerTransport:
28+
class DockerTransport(TransportProtocol):
2629
state: Literal["constructed", "initialized", "started", "stopped"] = "constructed"
2730

2831
def __init__(self, config: DockerServerParameters) -> None:
2932
self.config = config
3033
self.docker: Docker | None = None
3134
self.container: containers.DockerContainer | None = None
32-
self.attach_result: any = None
35+
self.attach_result: Any = None
3336

3437
self._reader_send: MemoryObjectSendStream[str]
3538
self._reader_recv: MemoryObjectReceiveStream[str]
@@ -116,6 +119,7 @@ async def write_stdin():
116119
self._task_group.start_soon(write_stdin)
117120

118121
self.state = "started"
122+
await asyncio.sleep(1)
119123

120124
async def stop(self) -> None:
121125
logger.debug("Stopping DockerTransport...")

src/easymcp/client/transports/stdio.py

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import shutil
55
import sys
6+
import gc
67
from typing import Literal
78

89
from loguru import logger
@@ -58,6 +59,7 @@ class StdioTransport(TransportProtocol):
5859
arguments: StdioServerParameters
5960
subprocess: asyncio.subprocess.Process | None
6061
read_buffer: ReadBuffer
62+
stderr_task: asyncio.Task | None
6163

6264
def __init__(self, arguments: StdioServerParameters):
6365
self.state = "constructed"
@@ -125,18 +127,52 @@ async def read_stderr(self) -> None:
125127
print(line.decode(), file=sys.stderr, end="")
126128

127129
async def stop(self) -> None:
128-
try:
129-
if self.subprocess:
130-
self.subprocess.terminate()
130+
if self.stderr_task:
131+
self.stderr_task.cancel()
132+
try:
133+
await self.stderr_task
134+
except asyncio.CancelledError:
135+
pass
136+
self.stderr_task = None
137+
138+
if self.subprocess:
139+
if self.subprocess.returncode is None:
131140
try:
141+
self.subprocess.terminate()
132142
await asyncio.wait_for(self.subprocess.wait(), timeout=5)
133143
except asyncio.TimeoutError:
134144
self.subprocess.kill()
135145
await self.subprocess.wait()
136146

137-
logger.info("Transport stopped successfully.")
138-
except Exception as e:
139-
logger.error(f"Error stopping transport: {e}")
140-
finally:
147+
if self.subprocess.stdin and not self.subprocess.stdin.is_closing():
148+
logger.debug("Closing stdin")
149+
self.subprocess.stdin.close()
150+
await self.subprocess.stdin.wait_closed()
151+
self.subprocess.stdin = None
152+
153+
if self.subprocess.stdout:
154+
logger.debug("Closing stdout")
155+
self.subprocess.stdout.feed_eof()
156+
try:
157+
await self.subprocess.stdout.read()
158+
except Exception:
159+
pass
160+
self.subprocess.stdout = None
161+
162+
if self.subprocess.stderr:
163+
logger.debug("Closing stderr")
164+
self.subprocess.stderr.feed_eof()
165+
try:
166+
await self.subprocess.stderr.read()
167+
except Exception:
168+
pass
169+
self.subprocess.stderr = None
170+
171+
await asyncio.sleep(0.1)
172+
gc.collect()
173+
141174
self.subprocess = None
142-
self.state = "stopped"
175+
176+
177+
self.state = "stopped"
178+
logger.info("Transport stopped successfully.")

tests/client_manager.py

Lines changed: 0 additions & 52 deletions
This file was deleted.

tests/docker_client_session.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

tests/name_formatter.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)