Skip to content

Commit 449840e

Browse files
authored
feat(core): Support List|Dict.py() (#55)
This PR introduces `List.py()` and `Dict.py()` which supports recursively converting MLC's list/dict back to Python's native list/dict
1 parent ba39078 commit 449840e

File tree

6 files changed

+75
-2
lines changed

6 files changed

+75
-2
lines changed

python/mlc/_cython/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
dtype_normalize,
2727
)
2828
from .core import ( # type: ignore[import-not-found]
29+
container_to_py,
2930
device_as_pair,
3031
dtype_as_triple,
3132
dtype_from_triple,

python/mlc/_cython/core.pyx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,6 +1418,18 @@ cpdef void tensor_init(PyAny self, object value):
14181418
self._mlc_any = ret._mlc_any
14191419
ret._mlc_any = _MLCAnyNone()
14201420

1421+
cpdef object container_to_py(object self):
1422+
from mlc.core.list import List as mlc_list
1423+
from mlc.core.dict import Dict as mlc_dict
1424+
1425+
if isinstance(self, mlc_list):
1426+
return [container_to_py(v) for v in self]
1427+
if isinstance(self, mlc_dict):
1428+
return {container_to_py(k): container_to_py(v) for k, v in self.items()}
1429+
if isinstance(self, Str):
1430+
return str(self)
1431+
return self
1432+
14211433
cpdef object tensor_data(PyAny self):
14221434
cdef DLTensor* tensor = _pyany_to_dl_tensor(self)
14231435
cdef void* data = tensor[0].data

python/mlc/core/dict.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from collections.abc import ItemsView, Iterable, Iterator, KeysView, Mapping, ValuesView
66
from typing import Any, TypeVar, overload
77

8-
from mlc._cython import MetaNoSlots, Ptr, c_class_core
8+
from mlc._cython import MetaNoSlots, Ptr, c_class_core, container_to_py
99

1010
from .object import Object
1111

@@ -134,6 +134,9 @@ def __eq__(self, other: Any) -> bool:
134134
def __ne__(self, other: Any) -> bool:
135135
return not (self == other)
136136

137+
def py(self) -> dict[K, V]:
138+
return container_to_py(self)
139+
137140

138141
class _DictKeysView(KeysView[K]):
139142
def __init__(self, mapping: Dict[K, V]) -> None:

python/mlc/core/list.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from collections.abc import Iterable, Iterator, Sequence
55
from typing import Any, TypeVar, overload
66

7-
from mlc._cython import MetaNoSlots, Ptr, c_class_core
7+
from mlc._cython import MetaNoSlots, Ptr, c_class_core, container_to_py
88

99
from .object import Object
1010

@@ -100,6 +100,9 @@ def __ne__(self, other: Any) -> bool:
100100
def __delitem__(self, i: int) -> None:
101101
self.pop(i)
102102

103+
def py(self) -> list[T]:
104+
return container_to_py(self)
105+
103106

104107
def _normalize_index(i: int, length: int) -> int:
105108
if not -length <= i < length:

tests/python/test_core_dict.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,32 @@ def test_dict_ne_1() -> None:
104104
b = {i: i * i for i in range(1, 5)}
105105
assert a != b
106106
assert b != a
107+
108+
109+
def test_dict_to_py_0() -> None:
110+
a = Dict({i: i * i for i in range(1, 5)}).py()
111+
assert isinstance(a, dict)
112+
assert len(a) == 4
113+
assert isinstance(a[1], int) and a[1] == 1
114+
assert isinstance(a[2], int) and a[2] == 4
115+
assert isinstance(a[3], int) and a[3] == 9
116+
assert isinstance(a[4], int) and a[4] == 16
117+
118+
119+
def test_dict_to_py_1() -> None:
120+
a = Dict(
121+
{
122+
"a": {
123+
"b": [2],
124+
"c": 3.0,
125+
},
126+
1: "one",
127+
None: "e",
128+
}
129+
).py()
130+
assert len(a) == 3 and set(a.keys()) == {"a", 1, None}
131+
assert isinstance(a["a"], dict) and len(a["a"]) == 2
132+
assert isinstance(a["a"]["b"], list) and len(a["a"]["b"]) == 1 and a["a"]["b"][0] == 2
133+
assert isinstance(a["a"]["c"], float) and a["a"]["c"] == 3.0
134+
assert isinstance(a[1], str) and a[1] == "one"
135+
assert isinstance(a[None], str) and a[None] == "e"

tests/python/test_core_list.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,28 @@ def test_list_delitem_out_of_by(seq: list[int], i: int) -> None:
229229
with pytest.raises(IndexError) as e:
230230
del a[i]
231231
assert str(e.value) == f"list index out of range: {i}"
232+
233+
234+
def test_list_to_py_0() -> None:
235+
a = List([1, 2, 3]).py()
236+
assert isinstance(a, list)
237+
238+
239+
def test_list_to_py_1() -> None:
240+
a = List([{"a": 1}, ["b"], 1, 2.0, "anything"]).py()
241+
assert isinstance(a, list)
242+
assert len(a) == 5
243+
assert isinstance(a[0], dict)
244+
assert isinstance(a[1], list)
245+
assert isinstance(a[2], int)
246+
assert isinstance(a[3], float)
247+
assert isinstance(a[4], str)
248+
assert isinstance(a[0], dict)
249+
# make sure those types are exactly Python's `str`, `int`, `float`
250+
assert len(a[0]) == 1 and isinstance(a[0], dict)
251+
assert a[0]["a"] == 1 and type(next(a[0].__iter__())) is str
252+
assert len(a[1]) == 1 and isinstance(a[1], list)
253+
assert a[1][0] == "b" and type(a[1][0]) is str
254+
assert a[2] == 1 and type(a[2]) is int
255+
assert a[3] == 2.0 and type(a[3]) is float
256+
assert a[4] == "anything" and type(a[4]) is str

0 commit comments

Comments
 (0)