Repository: narwhals-dev/narwhals
Pull request #608 (2024-07-24 08:42:34)

chore: extend `Implementation` for all backends
## What type of PR is this? (check all applicable)

- [x] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added 
- [x] Documented the changes


FBruzzesi: >  just a mypy issue

How dare?!
MarcoGorelli: really nice, thanks @FBruzzesi !

Maybe the signature is fine with `native_namespace` rather than `Implementation`? Because then, for anyone extending Narwhals, we give them a possibility of including `.from_dict` in their native namespace, whereas if they need to pass `Implementation`, then it's not clear how they can modify one of our enums

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..190018a0 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:
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