narwhals-map 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.3
2
+ Name: narwhals-map
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: danielgafni
6
+ Author-email: danielgafni <danielgafni16@gmail.com>
7
+ Requires-Dist: ibis-framework>=12.0.0 ; extra == 'ibis'
8
+ Requires-Dist: polars>=1.39.3 ; extra == 'polars'
9
+ Requires-Dist: polars-map>=0.2.0 ; extra == 'polars'
10
+ Requires-Dist: pyarrow>=23.0.1 ; extra == 'pyarrow'
11
+ Requires-Python: >=3.10
12
+ Provides-Extra: ibis
13
+ Provides-Extra: polars
14
+ Provides-Extra: pyarrow
15
+ Description-Content-Type: text/markdown
16
+
17
+ # narwhals-map
18
+
19
+ An experimental Narwhals plugin adding `Map` datatype.
20
+
21
+ > [!TIP]
22
+ > See [`narwhals-dev/narwhals#3525`](https://github.com/narwhals-dev/narwhals/issues/3525) issue for more info.
23
+
24
+ `Map` is natively supported across all backends except for Polars.
25
+
26
+ Supported Narwhals backends:
27
+ - Arrow
28
+ - Ibis
29
+ - Polars (via [`polars-map`](https://github.com/hafaio/polars-map))
30
+
31
+ Currently monkey-patches Narwhals to expose a new `nw.col.map` namespace.
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ import narwhals as nw
37
+ import narwhals_map # registers the Map dtype and .map namespace
38
+
39
+ df = nw.from_native(native_df) # use polars_map.Map for Polars-backed frames
40
+ result = df.select(nw.col("map_col").map.get("key1"))
41
+ ```
42
+
43
+ ## Known Limitations
44
+
45
+ - **`.to_arrow()` on Polars-backed frames**: Polars (via `polars-map`) stores maps as `List(Struct({key, value}))` internally. Calling `.to_arrow()` on a Polars-backed Narwhals frame produces `list<struct<key, value>>` columns instead of Arrow `map` types. PyArrow and Ibis backends are unaffected.
@@ -0,0 +1,29 @@
1
+ # narwhals-map
2
+
3
+ An experimental Narwhals plugin adding `Map` datatype.
4
+
5
+ > [!TIP]
6
+ > See [`narwhals-dev/narwhals#3525`](https://github.com/narwhals-dev/narwhals/issues/3525) issue for more info.
7
+
8
+ `Map` is natively supported across all backends except for Polars.
9
+
10
+ Supported Narwhals backends:
11
+ - Arrow
12
+ - Ibis
13
+ - Polars (via [`polars-map`](https://github.com/hafaio/polars-map))
14
+
15
+ Currently monkey-patches Narwhals to expose a new `nw.col.map` namespace.
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ import narwhals as nw
21
+ import narwhals_map # registers the Map dtype and .map namespace
22
+
23
+ df = nw.from_native(native_df) # use polars_map.Map for Polars-backed frames
24
+ result = df.select(nw.col("map_col").map.get("key1"))
25
+ ```
26
+
27
+ ## Known Limitations
28
+
29
+ - **`.to_arrow()` on Polars-backed frames**: Polars (via `polars-map`) stores maps as `List(Struct({key, value}))` internally. Calling `.to_arrow()` on a Polars-backed Narwhals frame produces `list<struct<key, value>>` columns instead of Arrow `map` types. PyArrow and Ibis backends are unaffected.
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "narwhals-map"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "danielgafni", email = "danielgafni16@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ pyarrow = [
15
+ "pyarrow>=23.0.1",
16
+ ]
17
+ polars = [
18
+ "polars>=1.39.3",
19
+ "polars-map>=0.2.0",
20
+ ]
21
+ ibis = [
22
+ "ibis-framework>=12.0.0",
23
+ ]
24
+
25
+ [build-system]
26
+ requires = ["uv_build>=0.10.4,<0.11.0"]
27
+ build-backend = "uv_build"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ # narwhals has to depend on us I guess?
32
+ # so we can't depend on it
33
+ "narwhals>=2.18.1",
34
+ "prek>=0.3.8",
35
+ "pyrefly>=0.59.1",
36
+ "pytest>=9.0.2",
37
+ "pytest-cases>=3.10.1",
38
+ "ruff>=0.15.9",
39
+ "ibis-framework[duckdb]>=12.0.0",
40
+ ]
@@ -0,0 +1,5 @@
1
+ import narwhals_map._narwhals_patch # noqa: F401 - applies monkey-patches at import time
2
+ from narwhals_map._dtype import Map
3
+ from narwhals_map._version import __version__
4
+
5
+ __all__ = ["Map", "__version__"]
File without changes
@@ -0,0 +1,18 @@
1
+ from typing import Any
2
+
3
+ from narwhals._compliant.expr import EagerExprNamespace
4
+
5
+
6
+ class ArrowExprMapNamespace(EagerExprNamespace): # type: ignore[type-arg]
7
+ def get(self, key: Any) -> Any:
8
+ compliant = self._compliant_expr
9
+
10
+ def inner(df): # type: ignore[no-untyped-def]
11
+ return [series.map.get(key=key) for series in compliant(df)] # type: ignore[attr-defined]
12
+
13
+ return compliant._from_callable(
14
+ inner,
15
+ evaluate_output_names=compliant._evaluate_output_names,
16
+ alias_output_names=compliant._alias_output_names,
17
+ context=compliant,
18
+ )
@@ -0,0 +1,14 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ import pyarrow.compute as pc
4
+ from narwhals._arrow.utils import ArrowSeriesNamespace
5
+
6
+ from narwhals_map._compliant.namespace import MapNamespace
7
+
8
+ if TYPE_CHECKING:
9
+ from narwhals._arrow.series import ArrowSeries
10
+
11
+
12
+ class ArrowSeriesMapNamespace(ArrowSeriesNamespace, MapNamespace["ArrowSeries"]):
13
+ def get(self, key: Any) -> "ArrowSeries":
14
+ return self.with_native(pc.map_lookup(self.native, key, "first")).alias(str(key)) # pyrefly: ignore [missing-attribute]
@@ -0,0 +1,14 @@
1
+ from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar
2
+
3
+ from narwhals._utils import CompliantT_co, _StoresCompliant
4
+
5
+ if TYPE_CHECKING:
6
+ from narwhals._compliant.typing import Accessor
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class MapNamespace(_StoresCompliant[CompliantT_co], Protocol[CompliantT_co]):
12
+ _accessor: ClassVar["Accessor"] = "map" # type: ignore[assignment]
13
+
14
+ def get(self, key: Any) -> CompliantT_co: ...
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import OrderedDict
4
+ from typing import TYPE_CHECKING
5
+
6
+ from narwhals.dtypes import DType, DTypeClass, NestedType
7
+
8
+ if TYPE_CHECKING:
9
+ from narwhals.typing import IntoDType
10
+
11
+
12
+ class Map(NestedType):
13
+ """Map composite type.
14
+
15
+ Arguments:
16
+ key: The key data type of the map. Must be hashable.
17
+ value: The value data type of the map.
18
+
19
+ Examples:
20
+ >>> import pyarrow as pa
21
+ >>> import narwhals as nw
22
+ >>> s_native = pa.map_(pa.string(), pa.int64())
23
+ ... [[("key1", 1), ("key2", 2)]]
24
+ ... )
25
+ >>> nw.from_native(s_native, series_only=True).dtype
26
+ Map(String, Int64)
27
+ """
28
+
29
+ __slots__ = (
30
+ "key",
31
+ "value",
32
+ )
33
+ key: IntoDType
34
+ value: IntoDType
35
+ """The key and value data types of the map."""
36
+
37
+ def __init__(self, key: IntoDType, value: IntoDType) -> None:
38
+ self.key = key
39
+ self.value = value
40
+
41
+ def __eq__(self, other: DType | type[DType]) -> bool: # type: ignore[override]
42
+ """Check if this Map is equivalent to another DType.
43
+
44
+ Examples:
45
+ >>> import narwhals as nw
46
+ >>> nw.Map(nw.Int64, nw.Int64) == nw.Map(nw.Int64, nw.Int64)
47
+ True
48
+ >>> nw.Map(nw.Int64, nw.Int64) == nw.Map(nw.Int64, nw.Boolean)
49
+ False
50
+
51
+ If a parent type is not specific about its inner type, we infer it as equal
52
+
53
+ >>> nw.Map(nw.Int64, nw.Int64) == nw.Map
54
+ True
55
+ """
56
+ if type(other) is DTypeClass and issubclass(other, self.__class__):
57
+ return True
58
+ if isinstance(other, self.__class__):
59
+ return (self.key, self.value) == (other.key, other.value)
60
+ return False
61
+
62
+ def __hash__(self) -> int:
63
+ return hash((self.__class__, (self.key, self.value)))
64
+
65
+ def __repr__(self) -> str:
66
+ class_name = self.__class__.__name__
67
+ return f"{class_name}({self.key}, {self.value})"
68
+
69
+ def to_schema(self) -> OrderedDict[str, IntoDType]:
70
+ """Return Map dtype as a schema dict."""
71
+ return OrderedDict({"key": self.key, "value": self.value})
File without changes
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from narwhals._compliant import LazyExprNamespace
6
+
7
+ from narwhals_map._compliant.namespace import MapNamespace
8
+
9
+ if TYPE_CHECKING:
10
+ from narwhals._ibis.expr import IbisExpr
11
+
12
+
13
+ class IbisExprMapNamespace(LazyExprNamespace["IbisExpr"], MapNamespace["IbisExpr"]):
14
+ def get(self, key: Any) -> IbisExpr:
15
+ return self.compliant._with_callable(lambda expr: expr.get(key).name(str(key)))
@@ -0,0 +1,105 @@
1
+ """Monkey-patch narwhals internals to add Map dtype and .map namespace support."""
2
+
3
+ from narwhals.expr import Expr
4
+ from narwhals.series import Series
5
+
6
+ from narwhals_map._dtype import Map
7
+ from narwhals_map.expr_map import ExprMapNamespace
8
+ from narwhals_map.series_map import SeriesMapNamespace
9
+
10
+ # --- Public API namespace patches (always available) ---
11
+
12
+ Expr.map = property(lambda self: ExprMapNamespace(self)) # type: ignore[attr-defined]
13
+ Series.map = property(lambda self: SeriesMapNamespace(self)) # type: ignore[attr-defined]
14
+
15
+ # --- PyArrow backend (optional) ---
16
+
17
+ try:
18
+ import pyarrow as pa
19
+ from narwhals._arrow import utils as _arrow_utils
20
+ from narwhals._arrow.series import ArrowSeries
21
+ from narwhals._compliant.expr import EagerExpr
22
+
23
+ from narwhals_map._arrow.expr_map import ArrowExprMapNamespace
24
+ from narwhals_map._arrow.series_map import ArrowSeriesMapNamespace
25
+
26
+ ArrowSeries.map = property(lambda self: ArrowSeriesMapNamespace(self)) # type: ignore[attr-defined]
27
+ EagerExpr.map = property(lambda self: ArrowExprMapNamespace(self)) # type: ignore[attr-defined]
28
+
29
+ _orig_arrow_native_to_narwhals_dtype = _arrow_utils.native_to_narwhals_dtype
30
+
31
+ def _patched_arrow_native_to_narwhals_dtype(dtype, version): # type: ignore[no-untyped-def]
32
+ if isinstance(dtype, pa.MapType):
33
+ return Map(
34
+ _patched_arrow_native_to_narwhals_dtype(dtype.key_type, version),
35
+ _patched_arrow_native_to_narwhals_dtype(dtype.item_type, version),
36
+ )
37
+ return _orig_arrow_native_to_narwhals_dtype(dtype, version)
38
+
39
+ _arrow_utils.native_to_narwhals_dtype = _patched_arrow_native_to_narwhals_dtype # type: ignore[assignment]
40
+
41
+ _orig_arrow_narwhals_to_native_dtype = _arrow_utils.narwhals_to_native_dtype
42
+
43
+ def _patched_arrow_narwhals_to_native_dtype(dtype, version): # type: ignore[no-untyped-def]
44
+ if isinstance(dtype, Map):
45
+ return pa.map_(
46
+ _orig_arrow_narwhals_to_native_dtype(dtype.key, version),
47
+ _orig_arrow_narwhals_to_native_dtype(dtype.value, version),
48
+ )
49
+ return _orig_arrow_narwhals_to_native_dtype(dtype, version)
50
+
51
+ _arrow_utils.narwhals_to_native_dtype = _patched_arrow_narwhals_to_native_dtype # type: ignore[assignment]
52
+ except ImportError:
53
+ pass
54
+
55
+ # --- Polars backend (optional) ---
56
+
57
+ try:
58
+ import polars_map as _polars_map
59
+ from narwhals._polars import utils as _polars_utils
60
+ from narwhals._polars.expr import PolarsExpr
61
+ from narwhals._polars.series import PolarsSeries
62
+
63
+ from narwhals_map._polars.expr_map import PolarsExprMapNamespace
64
+ from narwhals_map._polars.series_map import PolarsSeriesMapNamespace
65
+
66
+ PolarsExpr.map = property(lambda self: PolarsExprMapNamespace(self)) # type: ignore[attr-defined]
67
+ PolarsSeries.map = property(lambda self: PolarsSeriesMapNamespace(self)) # type: ignore[attr-defined]
68
+
69
+ _orig_polars_native_to_narwhals_dtype = _polars_utils.native_to_narwhals_dtype
70
+
71
+ def _patched_polars_native_to_narwhals_dtype(dtype, version): # type: ignore[no-untyped-def]
72
+ if isinstance(dtype, _polars_map.Map):
73
+ return Map(
74
+ _patched_polars_native_to_narwhals_dtype(dtype.key, version),
75
+ _patched_polars_native_to_narwhals_dtype(dtype.value, version),
76
+ )
77
+ return _orig_polars_native_to_narwhals_dtype(dtype, version)
78
+
79
+ _polars_utils.native_to_narwhals_dtype = _patched_polars_native_to_narwhals_dtype # type: ignore[assignment]
80
+ except ImportError:
81
+ pass
82
+
83
+ # --- Ibis backend (optional) ---
84
+
85
+ try:
86
+ from narwhals._ibis import utils as _ibis_utils
87
+ from narwhals._ibis.expr import IbisExpr
88
+
89
+ from narwhals_map._ibis.expr_map import IbisExprMapNamespace
90
+
91
+ IbisExpr.map = property(lambda self: IbisExprMapNamespace(self)) # type: ignore[attr-defined]
92
+
93
+ _orig_ibis_native_to_narwhals_dtype = _ibis_utils.native_to_narwhals_dtype
94
+
95
+ def _patched_ibis_native_to_narwhals_dtype(ibis_dtype, version): # type: ignore[no-untyped-def]
96
+ if ibis_dtype.is_map():
97
+ return Map(
98
+ _patched_ibis_native_to_narwhals_dtype(ibis_dtype.key_type, version),
99
+ _patched_ibis_native_to_narwhals_dtype(ibis_dtype.value_type, version),
100
+ )
101
+ return _orig_ibis_native_to_narwhals_dtype(ibis_dtype, version)
102
+
103
+ _ibis_utils.native_to_narwhals_dtype = _patched_ibis_native_to_narwhals_dtype # type: ignore[assignment]
104
+ except ImportError:
105
+ pass
@@ -0,0 +1,14 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ import polars_map # noqa: F401 - registers .map on pl.Expr
4
+ from narwhals._polars.expr import PolarsExprNamespace
5
+
6
+ from narwhals_map._compliant.namespace import MapNamespace
7
+
8
+ if TYPE_CHECKING:
9
+ from narwhals._polars.expr import PolarsExpr
10
+
11
+
12
+ class PolarsExprMapNamespace(PolarsExprNamespace, MapNamespace["PolarsExpr"]):
13
+ def get(self, key: Any) -> "PolarsExpr":
14
+ return self.compliant._with_native(self.native.map.get(key)) # pyrefly: ignore [missing-attribute]
@@ -0,0 +1,14 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ from narwhals._polars.series import PolarsSeriesNamespace
4
+
5
+ from narwhals_map._compliant.namespace import MapNamespace
6
+
7
+ if TYPE_CHECKING:
8
+ from narwhals._polars.series import PolarsSeries
9
+
10
+
11
+ class PolarsSeriesMapNamespace(PolarsSeriesNamespace, MapNamespace["PolarsSeries"]):
12
+ def get(self, key: Any) -> "PolarsSeries":
13
+ ns = self.__narwhals_namespace__()
14
+ return self.to_frame().select(ns.col(self.name).map.get(key)).get_column(str(key))
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,18 @@
1
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
2
+
3
+ from narwhals._expression_parsing import ExprKind, ExprNode
4
+
5
+ if TYPE_CHECKING:
6
+ from narwhals.expr import Expr
7
+
8
+ ExprT = TypeVar("ExprT", bound="Expr")
9
+
10
+
11
+ class ExprMapNamespace(Generic[ExprT]):
12
+ def __init__(self, expr: ExprT) -> None:
13
+ self._expr = expr
14
+
15
+ def get(self, key: Any) -> ExprT:
16
+ return self._expr._append_node(ExprNode(ExprKind.ELEMENTWISE, "map.get", key=key))._append_node(
17
+ ExprNode(ExprKind.ELEMENTWISE, "alias", name=str(key))
18
+ )
File without changes
@@ -0,0 +1,14 @@
1
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
2
+
3
+ if TYPE_CHECKING:
4
+ from narwhals.series import Series
5
+
6
+ SeriesT = TypeVar("SeriesT", bound="Series")
7
+
8
+
9
+ class SeriesMapNamespace(Generic[SeriesT]):
10
+ def __init__(self, series: SeriesT) -> None:
11
+ self._narwhals_series = series
12
+
13
+ def get(self, key: Any) -> SeriesT:
14
+ return self._narwhals_series._with_compliant(self._narwhals_series._compliant_series.map.get(key)) # pyrefly: ignore [missing-attribute]