Repository: narwhals-dev/narwhals
Pull request #609 (2024-07-24 09:08:25)

feat : added arrow `is_between` and `is_between_test.py`
## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [x] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Related issue # #372 
- Closes #

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.



anopsy: Okay so the  [XPASS(strict)]  issue seems to reappear in 2 checks, really curious about this one

Patch:
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 691d4899..abf3ff7c 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -15,6 +15,7 @@ from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pyarrow
 from narwhals.dependencies import get_pyarrow_compute
 from narwhals.dependencies import get_pyarrow_parquet
+from narwhals.utils import Implementation
 from narwhals.utils import flatten
 from narwhals.utils import generate_unique_token
 
@@ -34,7 +35,7 @@ class ArrowDataFrame:
         self, native_dataframe: Any, *, backend_version: tuple[int, ...]
     ) -> None:
         self._native_dataframe = native_dataframe
-        self._implementation = "arrow"  # for compatibility with PandasLikeDataFrame
+        self._implementation = Implementation.PYARROW
         self._backend_version = backend_version
 
     def __narwhals_namespace__(self) -> ArrowNamespace:
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index 24394ea1..4579b234 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -7,6 +7,7 @@ from typing import Literal
 
 from narwhals._expression_parsing import reuse_series_implementation
 from narwhals._expression_parsing import reuse_series_namespace_implementation
+from narwhals.utils import Implementation
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -34,7 +35,7 @@ class ArrowExpr:
         self._root_names = root_names
         self._depth = depth
         self._output_names = output_names
-        self._implementation = "arrow"
+        self._implementation = Implementation.PYARROW
         self._backend_version = backend_version
 
     def __repr__(self) -> str:  # pragma: no cover
diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py
index 37f39d43..fbb285b5 100644
--- a/narwhals/_arrow/namespace.py
+++ b/narwhals/_arrow/namespace.py
@@ -14,6 +14,7 @@ from narwhals._arrow.utils import horizontal_concat
 from narwhals._arrow.utils import vertical_concat
 from narwhals._expression_parsing import parse_into_exprs
 from narwhals.dependencies import get_pyarrow
+from narwhals.utils import Implementation
 
 if TYPE_CHECKING:
     from typing import Callable
@@ -98,6 +99,7 @@ class ArrowNamespace:
     # --- not in spec ---
     def __init__(self, *, backend_version: tuple[int, ...]) -> None:
         self._backend_version = backend_version
+        self._implementation = Implementation.PYARROW
 
     # --- selection ---
     def col(self, *column_names: str) -> ArrowExpr:
diff --git a/narwhals/_arrow/selectors.py b/narwhals/_arrow/selectors.py
index b8ba5bf5..f757ffed 100644
--- a/narwhals/_arrow/selectors.py
+++ b/narwhals/_arrow/selectors.py
@@ -6,6 +6,7 @@ from typing import NoReturn
 
 from narwhals import dtypes
 from narwhals._arrow.expr import ArrowExpr
+from narwhals.utils import Implementation
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -18,6 +19,7 @@ if TYPE_CHECKING:
 class ArrowSelectorNamespace:
     def __init__(self: Self, *, backend_version: tuple[int, ...]) -> None:
         self._backend_version = backend_version
+        self._implementation = Implementation.PYARROW
 
     def by_dtype(self: Self, dtypes: list[DType | type[DType]]) -> ArrowSelector:
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 7a13a53c..7f559e00 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -16,6 +16,7 @@ from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
 from narwhals.dependencies import get_pyarrow
 from narwhals.dependencies import get_pyarrow_compute
+from narwhals.utils import Implementation
 from narwhals.utils import generate_unique_token
 
 if TYPE_CHECKING:
@@ -32,7 +33,7 @@ class ArrowSeries:
     ) -> None:
         self._name = name
         self._native_series = native_series
-        self._implementation = "arrow"  # for compatibility with PandasLikeSeries
+        self._implementation = Implementation.PYARROW
         self._backend_version = backend_version
 
     def _from_native_series(self, series: Any) -> Self:
@@ -289,6 +290,29 @@ class ArrowSeries:
         pc = get_pyarrow_compute()
         return pc.all(self._native_series)  # type: ignore[no-any-return]
 
+    def is_between(self, lower_bound: Any, upper_bound: Any, closed: str = "both") -> Any:
+        pc = get_pyarrow_compute()
+        ser = self._native_series
+        if closed == "left":
+            ge = pc.greater_equal(ser, lower_bound)
+            lt = pc.less(ser, upper_bound)
+            res = pc.and_kleene(ge, lt)
+        elif closed == "right":
+            gt = pc.greater(ser, lower_bound)
+            le = pc.less_equal(ser, upper_bound)
+            res = pc.and_kleene(gt, le)
+        elif closed == "none":
+            gt = pc.greater(ser, lower_bound)
+            lt = pc.less(ser, upper_bound)
+            res = pc.and_kleene(gt, lt)
+        elif closed == "both":
+            ge = pc.greater_equal(ser, lower_bound)
+            le = pc.less_equal(ser, upper_bound)
+            res = pc.and_kleene(ge, le)
+        else:  # pragma: no cover
+            raise AssertionError
+        return self._from_native_series(res)
+
     def is_empty(self) -> bool:
         return len(self) == 0
 
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index 69556267..ca3c5138 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -11,7 +11,6 @@ from typing import overload
 
 from narwhals._expression_parsing import evaluate_into_exprs
 from narwhals._pandas_like.expr import PandasLikeExpr
-from narwhals._pandas_like.utils import Implementation
 from narwhals._pandas_like.utils import create_native_series
 from narwhals._pandas_like.utils import horizontal_concat
 from narwhals._pandas_like.utils import translate_dtype
@@ -21,6 +20,7 @@ from narwhals.dependencies import get_cudf
 from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
+from narwhals.utils import Implementation
 from narwhals.utils import flatten
 from narwhals.utils import generate_unique_token
 
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index 7ee28df3..945a054c 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
 
     from narwhals._pandas_like.dataframe import PandasLikeDataFrame
     from narwhals._pandas_like.namespace import PandasLikeNamespace
-    from narwhals._pandas_like.utils import Implementation
+    from narwhals.utils import Implementation
 
 
 class PandasLikeExpr:
diff --git a/narwhals/_pandas_like/group_by.py b/narwhals/_pandas_like/group_by.py
index bbfcadd9..129b047b 100644
--- a/narwhals/_pandas_like/group_by.py
+++ b/narwhals/_pandas_like/group_by.py
@@ -10,8 +10,8 @@ from typing import Iterator
 
 from narwhals._expression_parsing import is_simple_aggregation
 from narwhals._expression_parsing import parse_into_exprs
-from narwhals._pandas_like.utils import Implementation
 from narwhals._pandas_like.utils import native_series_from_iterable
+from narwhals.utils import Implementation
 from narwhals.utils import remove_prefix
 
 if TYPE_CHECKING:
diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py
index cd87070c..154e333b 100644
--- a/narwhals/_pandas_like/namespace.py
+++ b/narwhals/_pandas_like/namespace.py
@@ -18,7 +18,7 @@ from narwhals._pandas_like.utils import vertical_concat
 
 if TYPE_CHECKING:
     from narwhals._pandas_like.typing import IntoPandasLikeExpr
-    from narwhals._pandas_like.utils import Implementation
+    from narwhals.utils import Implementation
 
 
 class PandasLikeNamespace:
diff --git a/narwhals/_pandas_like/selectors.py b/narwhals/_pandas_like/selectors.py
index 44090fe1..8dd9ceae 100644
--- a/narwhals/_pandas_like/selectors.py
+++ b/narwhals/_pandas_like/selectors.py
@@ -10,8 +10,8 @@ from narwhals._pandas_like.expr import PandasLikeExpr
 if TYPE_CHECKING:
     from narwhals._pandas_like.dataframe import PandasLikeDataFrame
     from narwhals._pandas_like.series import PandasLikeSeries
-    from narwhals._pandas_like.utils import Implementation
     from narwhals.dtypes import DType
+    from narwhals.utils import Implementation
 
 
 class PandasSelectorNamespace:
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 900995f6..8c19f1ff 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -7,7 +7,6 @@ from typing import Literal
 from typing import Sequence
 from typing import overload
 
-from narwhals._pandas_like.utils import Implementation
 from narwhals._pandas_like.utils import int_dtype_mapper
 from narwhals._pandas_like.utils import native_series_from_iterable
 from narwhals._pandas_like.utils import reverse_translate_dtype
@@ -19,6 +18,7 @@ from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
 from narwhals.dependencies import get_pyarrow_compute
+from narwhals.utils import Implementation
 
 if TYPE_CHECKING:
     from typing_extensions import Self
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index b493cfed..f58c1297 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -1,7 +1,5 @@
 from __future__ import annotations
 
-from enum import Enum
-from enum import auto
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Iterable
@@ -10,6 +8,7 @@ from typing import TypeVar
 from narwhals.dependencies import get_cudf
 from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_pandas
+from narwhals.utils import Implementation
 from narwhals.utils import isinstance_or_issubclass
 
 T = TypeVar("T")
@@ -22,12 +21,6 @@ if TYPE_CHECKING:
     ExprT = TypeVar("ExprT", bound=PandasLikeExpr)
 
 
-class Implementation(Enum):
-    PANDAS = auto()
-    MODIN = auto()
-    CUDF = auto()
-
-
 def validate_column_comparand(index: Any, other: Any) -> Any:
     """Validate RHS of binary operation.
 
diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py
index 03190c3e..f137739f 100644
--- a/narwhals/_polars/dataframe.py
+++ b/narwhals/_polars/dataframe.py
@@ -7,6 +7,7 @@ from narwhals._polars.namespace import PolarsNamespace
 from narwhals._polars.utils import extract_args_kwargs
 from narwhals._polars.utils import translate_dtype
 from narwhals.dependencies import get_polars
+from narwhals.utils import Implementation
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -15,6 +16,7 @@ if TYPE_CHECKING:
 class PolarsDataFrame:
     def __init__(self, df: Any, *, backend_version: tuple[int, ...]) -> None:
         self._native_dataframe = df
+        self._implementation = Implementation.POLARS
         self._backend_version = backend_version
 
     def __repr__(self) -> str:  # pragma: no cover
diff --git a/narwhals/_polars/expr.py b/narwhals/_polars/expr.py
index 2fb69bdc..98aac298 100644
--- a/narwhals/_polars/expr.py
+++ b/narwhals/_polars/expr.py
@@ -3,19 +3,22 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 from typing import Any
 
-if TYPE_CHECKING:
-    from typing_extensions import Self
-
-    from narwhals.dtypes import DType
 from narwhals._polars.namespace import PolarsNamespace
 from narwhals._polars.utils import extract_args_kwargs
 from narwhals._polars.utils import extract_native
 from narwhals._polars.utils import reverse_translate_dtype
+from narwhals.utils import Implementation
+
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
+    from narwhals.dtypes import DType
 
 
 class PolarsExpr:
     def __init__(self, expr: Any) -> None:
         self._native_expr = expr
+        self._implementation = Implementation.POLARS
 
     def __repr__(self) -> str:  # pragma: no cover
         return "PolarsExpr"
diff --git a/narwhals/_polars/namespace.py b/narwhals/_polars/namespace.py
index d5c8307f..683b83c1 100644
--- a/narwhals/_polars/namespace.py
+++ b/narwhals/_polars/namespace.py
@@ -9,6 +9,7 @@ from narwhals import dtypes
 from narwhals._polars.utils import extract_args_kwargs
 from narwhals._polars.utils import reverse_translate_dtype
 from narwhals.dependencies import get_polars
+from narwhals.utils import Implementation
 
 if TYPE_CHECKING:
     from narwhals._polars.dataframe import PolarsDataFrame
@@ -39,6 +40,7 @@ class PolarsNamespace:
 
     def __init__(self, *, backend_version: tuple[int, ...]) -> None:
         self._backend_version = backend_version
+        self._implementation = Implementation.POLARS
 
     def __getattr__(self, attr: str) -> Any:
         from narwhals._polars.expr import PolarsExpr
diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py
index d64d4fe9..4a5fd2b7 100644
--- a/narwhals/_polars/series.py
+++ b/narwhals/_polars/series.py
@@ -8,6 +8,7 @@ from typing import overload
 from narwhals._polars.utils import extract_args_kwargs
 from narwhals._polars.utils import extract_native
 from narwhals.dependencies import get_polars
+from narwhals.utils import Implementation
 
 if TYPE_CHECKING:
     import numpy as np
@@ -26,6 +27,7 @@ PL = get_polars()
 class PolarsSeries:
     def __init__(self, series: Any, *, backend_version: tuple[int, ...]) -> None:
         self._native_series = series
+        self._implementation = Implementation.POLARS
         self._backend_version = backend_version
 
     def __repr__(self) -> str:  # pragma: no cover
diff --git a/narwhals/functions.py b/narwhals/functions.py
index 40316f67..1f9841de 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -11,12 +11,8 @@ from typing import Union
 
 from narwhals.dataframe import DataFrame
 from narwhals.dataframe import LazyFrame
-from narwhals.dependencies import get_cudf
-from narwhals.dependencies import get_modin
-from narwhals.dependencies import get_pandas
-from narwhals.dependencies import get_polars
-from narwhals.dependencies import get_pyarrow
 from narwhals.translate import from_native
+from narwhals.utils import Implementation
 from narwhals.utils import validate_laziness
 
 # Missing type parameters for generic type "DataFrame"
@@ -99,7 +95,8 @@ def from_dict(
         │ 2   ┆ 4   │
         └─────┴─────┘
     """
-    if native_namespace is get_polars():
+    implementation = Implementation.from_native_namespace(native_namespace)
+    if implementation is Implementation.POLARS:
         if schema:
             from narwhals._polars.utils import (
                 reverse_translate_dtype as polars_reverse_translate_dtype,
@@ -111,26 +108,18 @@ def from_dict(
             }
 
         native_frame = native_namespace.from_dict(data, schema=schema)
-    elif (
-        native_namespace is get_cudf()
-        or native_namespace is get_modin()
-        or native_namespace is get_pandas()
-    ):
+    elif implementation in {
+        Implementation.PANDAS,
+        Implementation.MODIN,
+        Implementation.CUDF,
+    }:
         native_frame = native_namespace.DataFrame.from_dict(data)
 
         if schema:
-            from narwhals._pandas_like.utils import Implementation
             from narwhals._pandas_like.utils import (
                 reverse_translate_dtype as pandas_like_reverse_translate_dtype,
             )
 
-            implementation = (
-                Implementation.PANDAS
-                if native_namespace is get_pandas()
-                else Implementation.MODIN
-                if native_namespace is get_modin()
-                else Implementation.CUDF
-            )
             schema = {
                 name: pandas_like_reverse_translate_dtype(
                     schema[name], native_type, implementation
@@ -139,7 +128,7 @@ def from_dict(
             }
             native_frame = native_frame.astype(schema)
 
-    elif native_namespace is get_pyarrow():
+    elif implementation is Implementation.PYARROW:
         if schema:
             from narwhals._arrow.utils import (
                 reverse_translate_dtype as arrow_reverse_translate_dtype,
diff --git a/narwhals/translate.py b/narwhals/translate.py
index f5b76dea..737753e5 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -283,13 +283,13 @@ def from_native(  # noqa: PLR0915
     from narwhals._interchange.dataframe import InterchangeFrame
     from narwhals._pandas_like.dataframe import PandasLikeDataFrame
     from narwhals._pandas_like.series import PandasLikeSeries
-    from narwhals._pandas_like.utils import Implementation
     from narwhals._polars.dataframe import PolarsDataFrame
     from narwhals._polars.dataframe import PolarsLazyFrame
     from narwhals._polars.series import PolarsSeries
     from narwhals.dataframe import DataFrame
     from narwhals.dataframe import LazyFrame
     from narwhals.series import Series
+    from narwhals.utils import Implementation
     from narwhals.utils import parse_version
 
     # Early returns
diff --git a/narwhals/utils.py b/narwhals/utils.py
index c586dfcf..d03c9227 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -2,6 +2,8 @@ from __future__ import annotations
 
 import re
 import secrets
+from enum import Enum
+from enum import auto
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Iterable
@@ -18,12 +20,45 @@ from narwhals.dependencies import get_pyarrow
 from narwhals.translate import to_native
 
 if TYPE_CHECKING:
+    from typing_extensions import Self
+
     from narwhals.dataframe import BaseFrame
     from narwhals.series import Series
 
 T = TypeVar("T")
 
 
+class Implementation(Enum):
+    PANDAS = auto()
+    MODIN = auto()
+    CUDF = auto()
+    PYARROW = auto()
+    POLARS = auto()
+
+    @classmethod
+    def from_native_namespace(
+        cls: type[Self], native_namespace: Any
+    ) -> Implementation:  # pragma: no cover
+        mapping = {
+            get_pandas(): Implementation.PANDAS,
+            get_modin(): Implementation.MODIN,
+            get_cudf(): Implementation.CUDF,
+            get_pyarrow(): Implementation.PYARROW,
+            get_polars(): Implementation.POLARS,
+        }
+        return mapping[native_namespace]
+
+    def to_native_namespace(self: Self) -> Any:  # pragma: no cover
+        mapping = {
+            Implementation.PANDAS: get_pandas(),
+            Implementation.MODIN: get_modin(),
+            Implementation.CUDF: get_cudf(),
+            Implementation.PYARROW: get_pyarrow(),
+            Implementation.POLARS: get_polars(),
+        }
+        return mapping[self]
+
+
 def remove_prefix(text: str, prefix: str) -> str:
     if text.startswith(prefix):
         return text[len(prefix) :]


Test patch:
diff --git a/tests/frame/join_test.py b/tests/frame/join_test.py
index d8ab380c..53ed3a14 100644
--- a/tests/frame/join_test.py
+++ b/tests/frame/join_test.py
@@ -7,7 +7,7 @@ import pandas as pd
 import pytest
 
 import narwhals.stable.v1 as nw
-from narwhals._pandas_like.utils import Implementation
+from narwhals.utils import Implementation
 from tests.utils import compare_dicts
 
 
diff --git a/tests/series_only/is_between_test.py b/tests/series_only/is_between_test.py
new file mode 100644
index 00000000..69097a31
--- /dev/null
+++ b/tests/series_only/is_between_test.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+import narwhals.stable.v1 as nw
+from tests.utils import compare_dicts
+
+data = [1, 4, 2, 5]
+
+
+@pytest.mark.parametrize(
+    ("closed", "expected"),
+    [
+        ("left", [True, True, True, False]),
+        ("right", [False, True, True, True]),
+        ("both", [True, True, True, True]),
+        ("none", [False, True, True, False]),
+    ],
+)
+def test_is_between(constructor_series: Any, closed: str, expected: list[bool]) -> None:
+    ser = nw.from_native(constructor_series(data), series_only=True)
+    result = ser.is_between(1, 5, closed=closed)
+    compare_dicts({"a": result}, {"a": expected})