Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Closed
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 Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ clean-docs:
uv run --isolated --all-packages --group docs $(MAKE) -C docs clean

doctest:
uv run --isolated --all-packages --group docs $(MAKE) -C docs doctest
uv run --isolated --all-packages --group docs --directory docs pytest --doctest-glob="*.md"

test-%: packages/%
uv run --isolated --directory $< pytest
Expand Down
16 changes: 16 additions & 0 deletions docs/source/api-reference/adapters/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import pytest
from jumpstarter_driver_composite.driver import Composite
from jumpstarter_driver_network.driver import EchoNetwork

from jumpstarter.common.utils import serve


@pytest.fixture
def network():
with serve(Composite(children={"tcp_port": EchoNetwork(), "unix_socket": EchoNetwork()})) as client:
yield client


@pytest.fixture(autouse=True)
def adapters_namespace(doctest_namespace, network):
doctest_namespace["network"] = network
35 changes: 14 additions & 21 deletions docs/source/api-reference/adapters/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,47 +39,55 @@ export:
### Forward a remote TCP port to a local TCP port

```{doctest}
>>> from jumpstarter_driver_network.adapters import TcpPortforwardAdapter
>>> # random port on localhost
>>> with TcpPortforwardAdapter(client=client.tcp_port) as addr:
>>> with TcpPortforwardAdapter(client=network.tcp_port) as addr:
... print(addr[0], addr[1])
127.0.0.1 ...
>>>
>>> # specific address and port
>>> with TcpPortforwardAdapter(client=client.tcp_port, local_host="127.0.0.2", local_port=8080) as addr:
>>> with TcpPortforwardAdapter(client=network.tcp_port, local_host="127.0.0.2", local_port=8080) as addr:
... print(addr[0], addr[1])
127.0.0.2 8080

```

### Forward a remote Unix domain socket to a local socket

```{doctest}
>>> with UnixPortforwardAdapter(client=client.unix_socket) as addr:
>>> from jumpstarter_driver_network.adapters import UnixPortforwardAdapter
>>> with UnixPortforwardAdapter(client=network.unix_socket) as addr:
... print(addr)
/tmp/jumpstarter-.../socket
>>> # the type of the remote socket and the local one doesn't have to match
>>> # e.g. forward a remote Unix domain socket to a local TCP port
>>> with TcpPortforwardAdapter(client=client.unix_socket) as addr:
>>> with TcpPortforwardAdapter(client=network.unix_socket) as addr:
... print(addr[0], addr[1])
127.0.0.1 ...

```

Connect to a remote TCP port with a web-based VNC client

```{doctest}
>>> with NovncAdapter(client=client.tcp_port) as url:
>>> from jumpstarter_driver_network.adapters import NovncAdapter
>>> with NovncAdapter(client=network.tcp_port) as url:
... print(url) # open the url in browser to access the VNC client
https://novnc.com/noVNC/vnc.html?autoconnect=1&reconnect=1&host=127.0.0.1&port=...

```

Interact with a remote TCP port as if it's a serial console

See [pexpect](https://pexpect.readthedocs.io/en/stable/api/fdpexpect.html) for API documentation

```{doctest}
>>> from jumpstarter_driver_network.adapters import PexpectAdapter
>>> # the server echos all inputs
>>> with PexpectAdapter(client=client.tcp_port) as expect:
>>> with PexpectAdapter(client=network.tcp_port) as expect:
... assert expect.send("hello") == 5 # written 5 bytes
... assert expect.expect(["hi", "hello"]) == 1 # found string at index 1

```

Connect to a remote TCP port with the fabric SSH client
Expand All @@ -91,18 +99,3 @@ See [fabric](https://docs.fabfile.org/en/latest/api/connection.html#fabric.conne
with FabricAdapter(client=client.tcp_port, connect_kwargs={"password": "secret"}) as conn:
conn.run("uname")
```

```{testsetup} *
from jumpstarter_driver_network.adapters import *
from jumpstarter_driver_network.driver import *
from jumpstarter_driver_composite.driver import Composite
from jumpstarter.common.utils import serve

instance = serve(Composite(children={"tcp_port": EchoNetwork(), "unix_socket": EchoNetwork()}))

client = instance.__enter__()
```

```{testcleanup} *
instance.__exit__(None, None, None)
```
108 changes: 53 additions & 55 deletions docs/source/api-reference/basedriver.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,60 +20,58 @@ This project is still evolving, so these docs may be incomplete or out-of-date.

## Example
```{testcode}
from sys import modules
from types import SimpleNamespace
from anyio import connect_tcp, sleep
from contextlib import asynccontextmanager
from collections.abc import Generator
from jumpstarter.driver import Driver, export, exportstream
from jumpstarter.client import DriverClient
from jumpstarter.common.utils import serve

class ExampleDriver(Driver):
@classmethod
def client(cls) -> str:
return f"example.ExampleClient"

@export
def echo(self, message) -> str:
return message

# driver calls can be either sync or async
@export
async def echo_async(self, message) -> str:
await sleep(5)
return message

@export
def echo_generator(self, message) -> Generator[str, None, None]:
for _ in range(10):
yield message

# stream constructor has to be an AsyncContextManager
# that yield an anyio.abc.ObjectStream
@exportstream
@asynccontextmanager
async def connect_tcp(self):
async with await connect_tcp(remote_host="example.com", remote_port=80) as stream:
yield stream

class ExampleClient(DriverClient):
# client methods are sync
def echo(self, message) -> str:
return self.call("echo", message)
# async driver methods can be invoked the same way
# return self.call("echo_async", message)

def echo_generator(self, message) -> Generator[str, None, None]:
yield from self.streamingcall("echo_generator", message)

modules["example"] = SimpleNamespace(ExampleClient=ExampleClient)

with serve(ExampleDriver()) as client:
print(client.echo("hello"))
assert list(client.echo_generator("hello")) == ["hello"] * 10
```

```{testoutput}
>>> from sys import modules
>>> from types import SimpleNamespace
>>> from anyio import connect_tcp, sleep
>>> from contextlib import asynccontextmanager
>>> from collections.abc import Generator
>>> from jumpstarter.driver import Driver, export, exportstream
>>> from jumpstarter.client import DriverClient
>>> from jumpstarter.common.utils import serve
>>>
>>> class ExampleDriver(Driver):
... @classmethod
... def client(cls) -> str:
... return f"example.ExampleClient"
...
... @export
... def echo(self, message) -> str:
... return message
...
... # driver calls can be either sync or async
... @export
... async def echo_async(self, message) -> str:
... await sleep(5)
... return message
...
... @export
... def echo_generator(self, message) -> Generator[str, None, None]:
... for _ in range(10):
... yield message
...
... # stream constructor has to be an AsyncContextManager
... # that yield an anyio.abc.ObjectStream
... @exportstream
... @asynccontextmanager
... async def connect_tcp(self):
... async with await connect_tcp(remote_host="example.com", remote_port=80) as stream:
... yield stream
>>>
>>> class ExampleClient(DriverClient):
... # client methods are sync
... def echo(self, message) -> str:
... return self.call("echo", message)
... # async driver methods can be invoked the same way
... # return self.call("echo_async", message)
...
... def echo_generator(self, message) -> Generator[str, None, None]:
... yield from self.streamingcall("echo_generator", message)
>>>
>>> modules["example"] = SimpleNamespace(ExampleClient=ExampleClient)
>>>
>>> with serve(ExampleDriver()) as client:
... print(client.echo("hello"))
... assert list(client.echo_generator("hello")) == ["hello"] * 10
hello

```
15 changes: 15 additions & 0 deletions docs/source/api-reference/drivers/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import pytest
from jumpstarter_driver_pyserial.driver import PySerial

from jumpstarter.common.utils import serve


@pytest.fixture
def pyserial():
with serve(PySerial(url="loop://")) as client:
yield client


@pytest.fixture(autouse=True)
def drivers_namespace(doctest_namespace, pyserial):
doctest_namespace["pyserial"] = pyserial
45 changes: 20 additions & 25 deletions docs/source/api-reference/drivers/pyserial.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,42 +31,37 @@ export:
## Examples
Using expect with a context manager
```{testcode}
with pyserialclient.pexpect() as session:
session.sendline("Hello, world!")
session.expect("Hello, world!")
>>> with pyserial.pexpect() as session:
... session.sendline("Hello, world!")
... session.expect("Hello, world!")
14
0

```

Using expect without a context manager
```{testcode}
session = pyserialclient.open()
session.sendline("Hello, world!")
session.expect("Hello, world!")
pyserialclient.close()
>>> session = pyserial.open()
>>> session.sendline("Hello, world!")
14
>>> session.expect("Hello, world!")
0
>>> pyserial.close()

```

Using a simple BlockingStream with a context manager
```{testcode}
with pyserialclient.stream() as stream:
stream.send(b"Hello, world!")
data = stream.receive()
>>> with pyserial.stream() as stream:
... stream.send(b"Hello, world!")
... data = stream.receive()

```

Using a simple BlockingStream without a context manager
```{testcode}
stream = pyserialclient.open_stream()
stream.send(b"Hello, world!")
data = stream.receive()
```

```{testsetup} *
from jumpstarter_driver_pyserial.driver import PySerial
from jumpstarter.common.utils import serve

instance = serve(PySerial(url="loop://"))

pyserialclient = instance.__enter__()
```
>>> stream = pyserial.open_stream()
>>> stream.send(b"Hello, world!")
>>> data = stream.receive()

```{testcleanup} *
instance.__exit__(None, None, None)
```
1 change: 1 addition & 0 deletions docs/source/api-reference/drivers/sdwire.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ exporter host.
Traceback (most recent call last):
...
FileNotFoundError: failed to find sd-wire device

```

## Client API
Expand Down
17 changes: 0 additions & 17 deletions docs/source/api-reference/drivers/tftp.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,5 @@ export:
... assert "test.txt" in files
...
... tftp.stop()
```

```{testsetup} *
import tempfile
import os
from jumpstarter_driver_tftp.driver import Tftp
from jumpstarter.common.utils import serve

# Create a persistent temp dir that won't be removed by the example
TEST_DIR = tempfile.mkdtemp(prefix='tftp-test-')
instance = serve(Tftp(root_dir=TEST_DIR, host="127.0.0.1"))
client = instance.__enter__()
```

```{testcleanup} *
instance.__exit__(None, None, None)
import shutil
shutil.rmtree(TEST_DIR, ignore_errors=True)
```
1 change: 1 addition & 0 deletions docs/source/api-reference/drivers/ustreamer.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ exposes both snapshot and streaming interfaces.
Traceback (most recent call last):
...
io.UnsupportedOperation: fileno

```

## Client API
Expand Down