Skip to content
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
12 changes: 12 additions & 0 deletions packages/cli/tests/bindings-test/src/test_do.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ async def test_sql_cursor_rows_read_written(env):
await stub.test_sql_cursor_rows_read_written()


@pytest.mark.asyncio
async def test_sql_cursor_iter(env):
stub = await _get_stub(env)
await stub.test_sql_cursor_iter()


@pytest.mark.asyncio
async def test_sql_cursor_toarray_getitem_int(env):
stub = await _get_stub(env)
await stub.test_sql_cursor_toarray_getitem_int()


@pytest.mark.asyncio
async def test_sql_database_size(env):
stub = await _get_stub(env)
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/tests/bindings-test/src/test_kv.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,18 @@ async def test_none_options_list(env):
await kv.put("_test:none_list", "val")
result = await kv.list(None)
assert result["list_complete"] is True


@pytest.mark.asyncio
async def test_binding_not_iterable(env):
kv = env.KV
with pytest.raises(TypeError, match="KvNamespace.*is not iterable"):
for _ in kv:
pass


@pytest.mark.asyncio
async def test_binding_no_len(env):
kv = env.KV
with pytest.raises(TypeError, match="KvNamespace.*has no len"):
len(kv)
47 changes: 47 additions & 0 deletions packages/cli/tests/bindings-test/src/worker_durable_object.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from contextlib import contextmanager

from workers import DurableObject


Expand Down Expand Up @@ -201,6 +203,51 @@ async def test_rpc_echo(self, value):
async def test_rpc_dict(self, data):
return {"received": data, "added": True}

@contextmanager
def _create_iter_table(self):
self.ctx.storage.sql.exec("DROP TABLE IF EXISTS test_iter")
self.ctx.storage.sql.exec(
"CREATE TABLE test_iter (id INTEGER PRIMARY KEY, val TEXT)"
)
self.ctx.storage.sql.exec(
"INSERT INTO test_iter (id, val) VALUES (?, ?)", 1, "alpha"
)
self.ctx.storage.sql.exec(
"INSERT INTO test_iter (id, val) VALUES (?, ?)", 2, "beta"
)
self.ctx.storage.sql.exec(
"INSERT INTO test_iter (id, val) VALUES (?, ?)", 3, "gamma"
)
try:
yield
finally:
self.ctx.storage.sql.exec("DROP TABLE IF EXISTS test_iter")

async def test_sql_cursor_iter(self):
with self._create_iter_table():
cursor = self.ctx.storage.sql.exec(
"SELECT id, val FROM test_iter ORDER BY id"
)
rows = [{"id": row["id"], "val": row["val"]} for row in cursor]
del cursor
assert len(rows) == 3, f"expected 3 rows, got {len(rows)}"
assert rows[0]["id"] == 1 and rows[0]["val"] == "alpha"
assert rows[1]["id"] == 2 and rows[1]["val"] == "beta"
assert rows[2]["id"] == 3 and rows[2]["val"] == "gamma"

async def test_sql_cursor_toarray_getitem_int(self):
with self._create_iter_table():
cursor = self.ctx.storage.sql.exec(
"SELECT id, val FROM test_iter ORDER BY id"
)
arr = cursor.toArray()
del cursor
first_row = arr[0]
assert first_row["id"] == 1 and first_row["val"] == "alpha", (
f"expected row with id=1, got {first_row!r}"
)
assert len(arr) == 3, f"expected len 3, got {len(arr)}"

async def test_storage_value_types(self):
await self.ctx.storage.deleteAll()
await self.ctx.storage.put("str", "hello")
Expand Down
23 changes: 23 additions & 0 deletions packages/runtime-sdk/src/workers/_workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,14 @@ class _BindingWrapper:
def __init__(self, binding):
self._binding = binding

@property
def _real_name(self):
js_name = _get_js_constructor_name(self._binding)
if not js_name:
# Should not happen, but just in case
return type(self).__name__
return js_name

def _should_wrap_nested_attribute(self, jsobj) -> bool:
if not isinstance(jsobj, JsProxy):
return False
Expand Down Expand Up @@ -1175,8 +1183,23 @@ def __getattr__(self, name):
return result

def __getitem__(self, key):
if isinstance(key, int):
return self._convert_result(self._binding[key])
return self._convert_result(getattr(self._binding, key))

def __iter__(self):
binding = self._binding
if not hasattr(binding, "__iter__"):
raise TypeError(f"'{self._real_name}' object is not iterable")
for item in binding:
yield self._convert_result(item)

def __len__(self):
binding = self._binding
if not hasattr(binding, "length"):
raise TypeError(f"'{self._real_name}' object has no len()")
return binding.length


class _FetcherWrapper(_BindingWrapper):
def fetch(self, *args, **kwargs):
Expand Down
Loading