Repository: narwhals-dev/narwhals
Pull request #1161 (2024-10-10 20:34:52)

chore: Use shiny's Makefile to allow for test commands to change over time
Related: 
* posit-dev/py-shiny#1729
* posit-dev/py-shiny#1731

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [x] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Re lated issue # NA
- Closes # NA

## Checklist

- [ ] Code follows style guide (ruff)
- [x] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.

The default `pytest` command does cover some tests, but others are ran under default settings. By integrating the make commands, `py-shiny` can update the internal script as things change without need for updates to `narwhals`.


Patch:
diff --git a/README.md b/README.md
index bcf314a4..44fc31e5 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@ Join the party!
 - [scikit-lego](https://github.com/koaning/scikit-lego)
 - [scikit-playtime](https://github.com/koaning/scikit-playtime)
 - [timebasedcv](https://github.com/FBruzzesi/timebasedcv)
+- [marimo](https://github.com/marimo-team/marimo)
 
 Feel free to add your project to the list if it's missing, and/or
 [chat with us on Discord](https://discord.gg/V3PqtB4VA4) if you'd like any support.
diff --git a/docs/installation.md b/docs/installation.md
index 76ff0504..e3cd8f6d 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -13,7 +13,7 @@ Then, if you start the Python REPL and see the following:
 ```python
 >>> import narwhals
 >>> narwhals.__version__
-'1.9.2'
+'1.9.3'
 ```
 then installation worked correctly!
 
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index 03d857d0..4f0b6ae8 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -59,7 +59,7 @@ from narwhals.utils import maybe_get_index
 from narwhals.utils import maybe_reset_index
 from narwhals.utils import maybe_set_index
 
-__version__ = "1.9.2"
+__version__ = "1.9.3"
 
 __all__ = [
     "dependencies",
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 4bab24a2..a0a4e16c 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -87,8 +87,7 @@ class ArrowDataFrame:
         self, *, named: bool = False
     ) -> list[tuple[Any, ...]] | list[dict[str, Any]]:
         if not named:
-            msg = "Unnamed rows are not yet supported on PyArrow tables"
-            raise NotImplementedError(msg)
+            return list(self.iter_rows(named=False))  # type: ignore[return-value]
         return self._native_frame.to_pylist()  # type: ignore[no-any-return]
 
     def iter_rows(
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index 6e96fb7c..6a629298 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -355,7 +355,6 @@ class PandasLikeDataFrame:
     ) -> Self:
         index = self._native_frame.index
         new_columns = evaluate_into_exprs(self, *exprs, **named_exprs)
-
         if not new_columns and len(self) == 0:
             return self
 
@@ -394,7 +393,11 @@ class PandasLikeDataFrame:
                 backend_version=self._backend_version,
             )
         else:
-            df = self._native_frame.copy(deep=False)
+            # This is the logic in pandas' DataFrame.assign
+            if self._backend_version < (2,):  # pragma: no cover
+                df = self._native_frame.copy(deep=True)
+            else:
+                df = self._native_frame.copy(deep=False)
             for s in new_columns:
                 df[s.name] = validate_dataframe_comparand(index, s)
         return self._from_native_frame(df)
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 2532aea8..2fe53b22 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -186,7 +186,7 @@ class PandasLikeSeries:
                 implementation=self._implementation,
                 backend_version=self._backend_version,
             )
-        s = self._native_series
+        s = self._native_series.copy(deep=True)
         s.iloc[indices] = values
         s.name = self.name
         return self._from_native_series(s)
diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py
index 07804219..9ca2f704 100644
--- a/narwhals/_polars/series.py
+++ b/narwhals/_polars/series.py
@@ -212,6 +212,12 @@ class PolarsSeries:
 
         return self._from_native_series(result)
 
+    def scatter(self, indices: int | Sequence[int], values: Any) -> Self:
+        values = extract_native(values)
+        s = self._native_series.clone()
+        s.scatter(indices, values)
+        return self._from_native_series(s)
+
     def value_counts(
         self: Self,
         *,
diff --git a/narwhals/series.py b/narwhals/series.py
index a9eb51a4..0115ac34 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -139,15 +139,29 @@ class Series:
         Set value(s) at given position(s).
 
         Arguments:
-           indices: Position(s) to set items at.
-           values: Values to set.
+            indices: Position(s) to set items at.
+            values: Values to set.
 
-        Warning:
-            For some libraries (pandas, Polars), this method operates in-place,
-            whereas for others (PyArrow) it doesn't!
-            We recommend being careful with it, and not relying on the
-            in-placeness. For example, a valid use case is when updating
-            a column in an eager dataframe, see the example below.
+        Note:
+            This method always returns a new Series, without modifying the original one.
+            Using this function in a for-loop is an anti-pattern, we recommend building
+            up your positions and values beforehand and doing an update in one go.
+
+            For example, instead of
+
+            ```python
+            for i in [1, 3, 2]:
+                value = some_function(i)
+                s = s.scatter(i, value)
+            ```
+
+            prefer
+
+            ```python
+            positions = [1, 3, 2]
+            values = [some_function(x) for x in positions]
+            s = s.scatter(positions, values)
+            ```
 
         Examples:
             >>> import pandas as pd
diff --git a/pyproject.toml b/pyproject.toml
index 92866a6a..5c1e6f96 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "narwhals"
-version = "1.9.2"
+version = "1.9.3"
 authors = [
   { name="Marco Gorelli", email="33491632+MarcoGorelli@users.noreply.github.com" },
 ]


Test patch:
diff --git a/.github/workflows/downstream_tests.yml b/.github/workflows/downstream_tests.yml
index 12f7f95b..d84129d7 100644
--- a/.github/workflows/downstream_tests.yml
+++ b/.github/workflows/downstream_tests.yml
@@ -113,22 +113,18 @@ jobs:
       - name: install-basics
         run: uv pip install --upgrade tox virtualenv setuptools --system
       - name: install-shiny-dev
+        env:
+          UV_SYSTEM_PYTHON: 1
         run: |
             cd py-shiny
-            uv pip install -e ".[dev,test]" --system
+            make narwhals-install-shiny
       - name: install-narwhals-dev
         run: |
             uv pip uninstall narwhals --system
             uv pip install -e . --system
       - name: show-deps
         run: uv pip freeze
-      - name: Run pytest
-        run: |
-            cd py-shiny
-            python tests/pytest/asyncio_prevent.py
-            pytest
-      - name: Run mypy
+      - name: Run `make narwhals-test-integration`
         run: |
             cd py-shiny
-            uv pip install mypy --system
-            mypy shiny
+            make narwhals-test-integration
diff --git a/tests/frame/rows_test.py b/tests/frame/rows_test.py
index 2d94ab18..744f6606 100644
--- a/tests/frame/rows_test.py
+++ b/tests/frame/rows_test.py
@@ -92,14 +92,6 @@ def test_rows(
     expected: list[tuple[Any, ...]] | list[dict[str, Any]],
 ) -> None:
     df = nw.from_native(df_raw, eager_only=True)
-    if isinstance(df_raw, pa.Table) and not named:
-        with pytest.raises(
-            NotImplementedError,
-            match="Unnamed rows are not yet supported on PyArrow tables",
-        ):
-            df.rows(named=named)
-        return
-
     result = df.rows(named=named)
     assert result == expected
 
diff --git a/tests/series_only/scatter_test.py b/tests/series_only/scatter_test.py
index 2edab2b8..0677a8dd 100644
--- a/tests/series_only/scatter_test.py
+++ b/tests/series_only/scatter_test.py
@@ -24,3 +24,27 @@ def test_scatter(constructor_eager: Any, request: pytest.FixtureRequest) -> None
         "b": [142, 132, 124],
     }
     compare_dicts(result, expected)
+
+
+def test_scatter_unchanged(constructor_eager: Any) -> None:
+    df = nw.from_native(
+        constructor_eager({"a": [1, 2, 3], "b": [142, 124, 132]}), eager_only=True
+    )
+    df.with_columns(
+        df["a"].scatter([0, 1], [999, 888]), df["b"].scatter([0, 2, 1], [142, 124, 132])
+    )
+    expected = {
+        "a": [1, 2, 3],
+        "b": [142, 124, 132],
+    }
+    compare_dicts(df, expected)
+
+
+def test_single_series(constructor_eager: Any) -> None:
+    df = nw.from_native(
+        constructor_eager({"a": [1, 2, 3], "b": [142, 124, 132]}), eager_only=True
+    )
+    s = df["a"]
+    s.scatter([0, 1], [999, 888])
+    expected = {"a": [1, 2, 3]}
+    compare_dicts({"a": s}, expected)