Skip to content

Commit d7c94d7

Browse files
committed
chore: make init awaitable
1 parent 183549b commit d7c94d7

File tree

9 files changed

+32
-80
lines changed

9 files changed

+32
-80
lines changed

packages/python/iii-example/src/iii.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
_iii: III | None = None
88

99

10-
def init_iii() -> III:
10+
async def init_iii() -> III:
1111
global _iii
1212
if _iii is None:
13-
_iii = init(
13+
_iii = await init(
1414
address=engine_ws_url,
1515
options=InitOptions(
1616
worker_name="iii-example",

packages/python/iii-example/src/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ async def _post_example(req: ApiRequest, ctx) -> ApiResponse:
187187
async def _async_main() -> None:
188188
from .iii import init_iii
189189

190-
init_iii()
190+
await init_iii()
191191
_setup()
192192

193193
while True:

packages/python/iii-example/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/python/iii/src/iii/__init__.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""III SDK for Python."""
22

3-
import asyncio
43
import logging
54

65
from .context import Context, get_context, with_context
@@ -32,19 +31,10 @@
3231
from .types import ApiRequest, ApiResponse, FunctionsAvailableCallback, RemoteFunctionHandler
3332

3433

35-
def init(address: str, options: InitOptions | None = None) -> III:
36-
"""Create an III client and auto-start its connection task."""
34+
async def init(address: str, options: InitOptions | None = None) -> III:
35+
"""Create an III client and connect to the server."""
3736
client = III(address, options)
38-
39-
try:
40-
loop = asyncio.get_running_loop()
41-
except RuntimeError as exc:
42-
raise RuntimeError(
43-
"iii.init() requires an active asyncio event loop. "
44-
"Call it inside async code or use `client = III(...); await client.connect()`"
45-
) from exc
46-
47-
loop.create_task(client.connect())
37+
await client.connect()
4838
return client
4939

5040

packages/python/iii/tests/test_iii_registration_dedup.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,17 @@
1010

1111

1212
@pytest.fixture(autouse=True)
13-
def reset_otel():
14-
yield
15-
# III.connect() calls init_otel() which sets global providers;
16-
# reset them so subsequent test files start with a clean slate.
17-
try:
18-
from iii.telemetry import shutdown_otel
19-
shutdown_otel()
20-
except Exception:
21-
pass
22-
try:
23-
import opentelemetry._logs._internal as _li
24-
_li._LOGGER_PROVIDER = None
25-
_li._LOGGER_PROVIDER_SET_ONCE._done = False
26-
except Exception:
27-
pass
28-
try:
29-
import opentelemetry.trace._internal as _ti
30-
_ti._TRACER_PROVIDER = None
31-
_ti._TRACER_PROVIDER_SET_ONCE._done = False
32-
except Exception:
33-
pass
34-
try:
35-
import opentelemetry.metrics._internal as _mi
36-
_mi._METER_PROVIDER = None
37-
_mi._METER_PROVIDER_SET_ONCE._done = False
38-
except Exception:
39-
pass
13+
def disable_otel(monkeypatch: pytest.MonkeyPatch):
14+
"""Prevent OTel from starting a SharedEngineConnection during these tests.
15+
16+
The monkeypatched websockets.connect returns a plain coroutine, not an
17+
async-context-manager, so SharedEngineConnection._run() would produce
18+
'coroutine was never awaited' warnings.
19+
"""
20+
import iii.telemetry as telemetry_mod
21+
22+
monkeypatch.setattr(telemetry_mod, "init_otel", lambda config=None, loop=None: None)
23+
monkeypatch.setattr(telemetry_mod, "attach_event_loop", lambda loop: None)
4024

4125

4226
class FakeWebSocket:

packages/python/iii/tests/test_init_api.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,18 @@ def anyio_backend() -> str:
1111

1212

1313
@pytest.mark.anyio
14-
async def test_init_schedules_connect(monkeypatch: pytest.MonkeyPatch) -> None:
15-
called = asyncio.Event()
14+
async def test_init_awaits_connect(monkeypatch: pytest.MonkeyPatch) -> None:
15+
called = False
1616

1717
async def fake_connect(self: III) -> None:
18-
called.set()
18+
nonlocal called
19+
called = True
1920

2021
monkeypatch.setattr(III, "connect", fake_connect)
2122

22-
client = init("ws://fake")
23+
client = await init("ws://fake")
2324
assert isinstance(client, III)
24-
25-
await asyncio.wait_for(called.wait(), timeout=0.2)
26-
27-
28-
def test_init_requires_running_loop() -> None:
29-
with pytest.raises(RuntimeError, match="active asyncio event loop"):
30-
init("ws://fake")
25+
assert called
3126

3227

3328
@pytest.mark.anyio
@@ -49,14 +44,11 @@ async def fake_do_connect(self: III) -> None:
4944
monkeypatch.setattr(telemetry, "attach_event_loop", fake_attach_event_loop)
5045
monkeypatch.setattr(III, "_do_connect", fake_do_connect)
5146

52-
client = init(
47+
client = await init(
5348
"ws://fake",
5449
InitOptions(otel={"enabled": True, "service_name": "iii-python-init-test"}),
5550
)
5651

57-
# let scheduled connect task run
58-
await asyncio.sleep(0)
59-
6052
assert isinstance(client, III)
6153
assert captured["config"] is not None
6254
assert getattr(captured["config"], "service_name", None) == "iii-python-init-test"

packages/rust/iii-example/src/main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1414
otel: Some(OtelConfig::default()),
1515
..Default::default()
1616
},
17-
)?;
17+
)
18+
.await?;
1819

1920
// Register HTTP fetch API handlers (GET & POST http-fetch with OTel instrumentation)
2021
http_example::setup(&iii);

packages/rust/iii/src/lib.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub struct InitOptions {
3232
pub otel: Option<crate::telemetry::types::OtelConfig>,
3333
}
3434

35-
pub fn init(address: &str, options: InitOptions) -> Result<III, IIIError> {
35+
pub async fn init(address: &str, options: InitOptions) -> Result<III, IIIError> {
3636
let InitOptions {
3737
metadata,
3838
#[cfg(feature = "otel")]
@@ -50,15 +50,7 @@ pub fn init(address: &str, options: InitOptions) -> Result<III, IIIError> {
5050
iii.set_otel_config(cfg);
5151
}
5252

53-
let handle = tokio::runtime::Handle::try_current()
54-
.map_err(|_| IIIError::Runtime("iii_sdk::init requires an active Tokio runtime".into()))?;
55-
56-
let client = iii.clone();
57-
handle.spawn(async move {
58-
if let Err(err) = client.connect().await {
59-
tracing::warn!(error = %err, "iii_sdk::init auto-connect failed");
60-
}
61-
});
53+
iii.connect().await?;
6254

6355
Ok(iii)
6456
}

packages/rust/iii/tests/init_api.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
1-
use iii_sdk::{IIIError, InitOptions, init};
2-
3-
#[test]
4-
fn init_without_runtime_returns_runtime_error() {
5-
match init("ws://127.0.0.1:49134", InitOptions::default()) {
6-
Err(IIIError::Runtime(_)) => {}
7-
Err(other) => panic!("expected Runtime error, got {other:?}"),
8-
Ok(_) => panic!("expected init to fail without Tokio runtime"),
9-
}
10-
}
1+
use iii_sdk::{InitOptions, init};
112

123
#[tokio::test]
134
async fn init_with_runtime_returns_sdk_instance() {
145
let client = init("ws://127.0.0.1:49134", InitOptions::default())
6+
.await
157
.expect("init should succeed inside Tokio runtime");
168

179
// API should remain usable immediately after init()
@@ -33,6 +25,7 @@ async fn init_applies_otel_config_before_auto_connect() {
3325
..Default::default()
3426
},
3527
)
28+
.await
3629
.expect("init should succeed");
3730

3831
client.register_function("test.echo.otel", |input| async move { Ok(input) });

0 commit comments

Comments
 (0)