Repository: narwhals-dev/narwhals
Pull request #303 (2024-06-15 11:10:36)

ci: issue template correction
<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [x] 🐳 Other

## Related issues 

- Related issue #https://github.com/narwhals-dev/narwhals/issues/218
- Closes #

## Checklist

- [ ] Code follows style guide (ruff)
- [ ] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.

The template for the docs issues was not showing up, it had errors 😱. 



Patch:
diff --git a/.github/ISSUE_TEMPLATE/doc_issue.yml b/.github/ISSUE_TEMPLATE/doc_issue.yml
index bc09deaf..954320c9 100644
--- a/.github/ISSUE_TEMPLATE/doc_issue.yml
+++ b/.github/ISSUE_TEMPLATE/doc_issue.yml
@@ -4,8 +4,8 @@ title: "[Doc]: "
 
 body:
 - type: markdown
-  attibutes:
-    values: > 
+  attributes:
+    value: > 
          ## Thanks for taking the time to fill out this form
 - type: dropdown
   id: TYPE
@@ -14,26 +14,22 @@ body:
     options: 
       - 'Correction'
       - 'Improvement'
-    validations:
+  validations:
       required: true  
 - type: textarea
   attributes:
     label: Please describe the issue.
     description: >
       Tell us if something is unclear or incorrect, and where.
-    validations:
+  validations:
       required: true
 - type: textarea
   attributes:
     label: If you have a suggestion on how it should be, add it below.
     description: >
       How can we improve it
-- type: dropdown
-  id: PR
+- type: markdown
   attributes:
-    label: Would you want to open a pull request to fix it?
-    options:
-      - 'yes'
-      - 'no'
-      
-           
+    value: > 
+        ### If you'd be interested in opening a pull request to fix this, please let us know!
+      
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 4cbdff45..92ffcad0 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -38,17 +38,12 @@ body:
     label: If you have tried alternatives, please describe them below.
     description: >
        What you have tried if applicable.
-- type: dropdown
-  id: PR
-  attributes:
-    label: Would you want to open a pull request?
-    options:
-      - 'yes'
-      - 'no'
-  validations:
-      required: false
 - type: textarea
   attributes:
     label: Additional information that may help us understand your needs.
     description: >
        Context or useful information
+- type: markdown
+  attributes:
+    value: > 
+        ### If you'd be interested in opening a pull request to fix this, please let us know!
diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml
index a9b68a5b..7ee320bd 100644
--- a/.github/workflows/publish_to_pypi.yml
+++ b/.github/workflows/publish_to_pypi.yml
@@ -49,37 +49,3 @@ jobs:
     - name: Publish distribution 📦 to PyPI
       uses: pypa/gh-action-pypi-publish@release/v1
 
-  github-release:
-    name: >-
-      Sign the Python 🐍 distribution 📦 with Sigstore
-      and upload them to GitHub Release
-    needs:
-    - publish-to-pypi
-    runs-on: ubuntu-latest
-
-    permissions:
-      contents: write  # IMPORTANT: mandatory for making GitHub Releases
-      id-token: write  # IMPORTANT: mandatory for sigstore
-
-    steps:
-    - name: Download all the dists
-      uses: actions/download-artifact@v3
-      with:
-        name: python-package-distributions
-        path: dist/
-    - name: Sign the dists with Sigstore
-      uses: sigstore/gh-action-sigstore-python@v2.1.1
-      with:
-        inputs: >-
-          ./dist/*.tar.gz
-          ./dist/*.whl
-    - name: Upload artifact signatures to GitHub Release
-      env:
-        GITHUB_TOKEN: ${{ github.token }}
-      # Upload to GitHub Release using the `gh` CLI.
-      # `dist/` contains the built packages, and the
-      # sigstore-produced signatures and certificates.
-      run: >-
-        gh release upload
-        '${{ github.ref_name }}' dist/**
-        --repo '${{ github.repository }}'
diff --git a/docs/installation.md b/docs/installation.md
index 08090f19..35430f65 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -11,6 +11,6 @@ Then, if you start the Python REPL and see the following:
 ```python
 >>> import narwhals
 >>> narwhals.__version__
-'0.9.9'
+'0.9.10'
 ```
 then installation worked correctly!
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index 891b8759..a97f60a3 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -35,7 +35,7 @@ from narwhals.utils import maybe_align_index
 from narwhals.utils import maybe_convert_dtypes
 from narwhals.utils import maybe_set_index
 
-__version__ = "0.9.9"
+__version__ = "0.9.10"
 
 __all__ = [
     "selectors",
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index a7e167f0..97c7f821 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -315,6 +315,15 @@ class PandasExprStringNamespace:
             suffix,
         )
 
+    def contains(self, pattern: str, *, literal: bool) -> PandasExpr:
+        return reuse_series_namespace_implementation(
+            self._expr,
+            "str",
+            "contains",
+            pattern,
+            literal=literal,
+        )
+
     def head(self, n: int = 5) -> PandasExpr:
         return reuse_series_namespace_implementation(
             self._expr,
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index e1e6cd30..61d61fbc 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -518,6 +518,11 @@ class PandasSeriesStringNamespace:
             self._series._series.str.endswith(suffix),
         )
 
+    def contains(self, pattern: str, *, literal: bool = False) -> PandasSeries:
+        return self._series._from_series(
+            self._series._series.str.contains(pat=pattern, regex=not literal)
+        )
+
     def head(self, n: int = 5) -> PandasSeries:
         return self._series._from_series(
             self._series._series.str[:n],
diff --git a/narwhals/expression.py b/narwhals/expression.py
index 3ee39781..d6829c3c 100644
--- a/narwhals/expression.py
+++ b/narwhals/expression.py
@@ -1537,6 +1537,65 @@ class ExprStringNamespace:
             lambda plx: self._expr._call(plx).str.ends_with(suffix)
         )
 
+    def contains(self, pattern: str, *, literal: bool = False) -> Expr:
+        """
+        Check if string contains a substring that matches a pattern.
+
+        Arguments:
+            pattern: A Character sequence or valid regular expression pattern.
+
+            literal: If True, treats the pattern as a literal string.
+                     If False, assumes the pattern is a regular expression.
+
+        Example:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> import narwhals as nw
+            >>> data = {"pets": ["cat", "dog", "rabbit and parrot", "dove", None]}
+            >>> df_pd = pd.DataFrame(data)
+            >>> df_pl = pl.DataFrame(data)
+
+            We define a dataframe-agnostic function:
+
+            >>> @nw.narwhalify
+            ... def func(df):
+            ...     return df.with_columns(
+            ...         default_match=nw.col("pets").str.contains("parrot|Dove"),
+            ...         case_insensitive_match=nw.col("pets").str.contains("(?i)parrot|Dove"),
+            ...         literal_match=nw.col("pets").str.contains(
+            ...             "parrot|Dove", literal=True
+            ...         ),
+            ...     )
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(df_pd)
+                            pets default_match case_insensitive_match literal_match
+            0                cat         False                  False         False
+            1                dog         False                  False         False
+            2  rabbit and parrot          True                   True         False
+            3               dove         False                   True         False
+            4               None          None                   None          None
+            >>> func(df_pl)
+            shape: (5, 4)
+            ┌───────────────────┬───────────────┬────────────────────────┬───────────────┐
+            │ pets              ┆ default_match ┆ case_insensitive_match ┆ literal_match │
+            │ ---               ┆ ---           ┆ ---                    ┆ ---           │
+            │ str               ┆ bool          ┆ bool                   ┆ bool          │
+            ╞═══════════════════╪═══════════════╪════════════════════════╪═══════════════╡
+            │ cat               ┆ false         ┆ false                  ┆ false         │
+            │ dog               ┆ false         ┆ false                  ┆ false         │
+            │ rabbit and parrot ┆ true          ┆ true                   ┆ false         │
+            │ dove              ┆ false         ┆ true                   ┆ false         │
+            │ null              ┆ null          ┆ null                   ┆ null          │
+            └───────────────────┴───────────────┴────────────────────────┴───────────────┘
+
+        """
+
+        return self._expr.__class__(
+            lambda plx: self._expr._call(plx).str.contains(pattern, literal=literal)
+        )
+
     def head(self, n: int = 5) -> Expr:
         """
         Take the first n elements of each string.
diff --git a/narwhals/series.py b/narwhals/series.py
index 81477565..1adc55e4 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -1723,6 +1723,54 @@ class SeriesStringNamespace:
     def ends_with(self, suffix: str) -> Series:
         return self._series.__class__(self._series._series.str.ends_with(suffix))
 
+    def contains(self, pattern: str, *, literal: bool = False) -> Series:
+        """
+        Check if string contains a substring that matches a pattern.
+
+        Arguments:
+            pattern: A Character sequence or valid regular expression pattern.
+
+            literal: If True, treats the pattern as a literal string.
+                     If False, assumes the pattern is a regular expression.
+
+        Example:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> import narwhals as nw
+            >>> pets = ["cat", "dog", "rabbit and parrot", "dove", None]
+            >>> s_pd = pd.Series(pets)
+            >>> s_pl = pl.Series(pets)
+
+            We define a dataframe-agnostic function:
+
+            >>> @nw.narwhalify(series_only=True)
+            ... def func(s):
+            ...     return s.str.contains("parrot|dove")
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(s_pd)
+            0    False
+            1    False
+            2     True
+            3     True
+            4     None
+            dtype: object
+            >>> func(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            shape: (5,)
+            Series: '' [bool]
+            [
+               false
+               false
+               true
+               true
+               null
+            ]
+        """
+        return self._series.__class__(
+            self._series._series.str.contains(pattern, literal=literal)
+        )
+
     def head(self, n: int = 5) -> Series:
         """
         Take the first n elements of each string.
diff --git a/pyproject.toml b/pyproject.toml
index a9c41443..9f007007 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "narwhals"
-version = "0.9.9"
+version = "0.9.10"
 authors = [
   { name="Marco Gorelli", email="33491632+MarcoGorelli@users.noreply.github.com" },
 ]


Test patch:
diff --git a/tests/expr/str/contains_test.py b/tests/expr/str/contains_test.py
new file mode 100644
index 00000000..e38c1be0
--- /dev/null
+++ b/tests/expr/str/contains_test.py
@@ -0,0 +1,30 @@
+from typing import Any
+
+import pandas as pd
+import polars as pl
+import pytest
+
+import narwhals as nw
+from tests.utils import compare_dicts
+
+data = {"pets": ["cat", "dog", "rabbit and parrot", "dove"]}
+
+df_pandas = pd.DataFrame(data)
+df_polars = pl.DataFrame(data)
+
+
+@pytest.mark.parametrize("df_any", [df_pandas, df_polars])
+def test_contains(df_any: Any) -> None:
+    df = nw.from_native(df_any, eager_only=True)
+    result = df.with_columns(
+        case_insensitive_match=nw.col("pets").str.contains("(?i)parrot|Dove")
+    )
+    expected = {
+        "pets": ["cat", "dog", "rabbit and parrot", "dove"],
+        "case_insensitive_match": [False, False, True, True],
+    }
+    compare_dicts(result, expected)
+    result = df.with_columns(
+        case_insensitive_match=df["pets"].str.contains("(?i)parrot|Dove")
+    )
+    compare_dicts(result, expected)