Repository: dask/dask

Issue:
Add string methods removeprefix and removesuffix
Since [Pandas 1.4.0 release](https://pandas.pydata.org/pandas-docs/stable/whatsnew/v1.4.0.html) (see issue https://github.com/pandas-dev/pandas/issues/36944), two new string methods are available, as a result of Python 3.9 incorporating them as native `str` methods:
* [Series.str.removeprefix](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.str.removeprefix.html), which applies [str.removeprefix](https://docs.python.org/3.9/library/stdtypes.html?highlight=strip#str.removeprefix) to a whole `Series`.
* [Series.str.removesuffix](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.str.removesuffix.html), which applies [str.removesuffix](https://docs.python.org/3.9/library/stdtypes.html?highlight=strip#str.removesuffix) to a whole `Series`.
 
So this issue is about adding these methods to Dask (which should be quite straightforward).

As a temporary workaround, they should be marked explicitly as `NotImplemented`, the same way it's done for `.str.get_dummies` (see line below):

https://github.com/dask/dask/blob/43f33a32dd5cdf77363b6ff6348b5f1a8282d28c/dask/dataframe/accessor.py#L119

Please note: **if we want to be compatible with Python < 3.9 or Pandas < 1.4, the implementation shouldn't rely on the standard library's existence of these two methods**. For example, in Python 3.8 they weren't implemented yet.


Discussion:
Thanks @jorloplaz -- I agree this would be a nice addition. Is this something you're interested in working on? 
I'll see what I can do. This'd be my 1st issue and you tagged it as a good 2nd one, but I think I can manage.

Resolving patch:
diff --git a/dask/dataframe/accessor.py b/dask/dataframe/accessor.py
--- a/dask/dataframe/accessor.py
+++ b/dask/dataframe/accessor.py
@@ -8,7 +8,7 @@
 from dask.utils import derived_from
 
 
-def _bind_method(cls, pd_cls, attr):
+def _bind_method(cls, pd_cls, attr, min_version=None):
     def func(self, *args, **kwargs):
         return self._function_map(attr, *args, **kwargs)
 
@@ -18,10 +18,10 @@ def func(self, *args, **kwargs):
         func.__wrapped__ = getattr(pd_cls, attr)
     except Exception:
         pass
-    setattr(cls, attr, derived_from(pd_cls)(func))
+    setattr(cls, attr, derived_from(pd_cls, version=min_version)(func))
 
 
-def _bind_property(cls, pd_cls, attr):
+def _bind_property(cls, pd_cls, attr, min_version=None):
     def func(self):
         return self._property_map(attr)
 
@@ -31,7 +31,7 @@ def func(self):
         func.__wrapped__ = getattr(pd_cls, attr)
     except Exception:
         pass
-    setattr(cls, attr, property(derived_from(pd_cls)(func)))
+    setattr(cls, attr, property(derived_from(pd_cls, version=min_version)(func)))
 
 
 def maybe_wrap_pandas(obj, x):
@@ -70,12 +70,14 @@ def __init_subclass__(cls, **kwargs):
         """Bind all auto-generated methods & properties"""
         super().__init_subclass__(**kwargs)
         pd_cls = getattr(pd.Series, cls._accessor_name)
-        for attr in cls._accessor_methods:
+        for item in cls._accessor_methods:
+            attr, min_version = item if isinstance(item, tuple) else (item, None)
             if not hasattr(cls, attr):
-                _bind_method(cls, pd_cls, attr)
-        for attr in cls._accessor_properties:
+                _bind_method(cls, pd_cls, attr, min_version)
+        for item in cls._accessor_properties:
+            attr, min_version = item if isinstance(item, tuple) else (item, None)
             if not hasattr(cls, attr):
-                _bind_property(cls, pd_cls, attr)
+                _bind_property(cls, pd_cls, attr, min_version)
 
     @staticmethod
     def _delegate_property(obj, accessor, attr):
@@ -229,6 +231,8 @@ class StringAccessor(Accessor):
         "normalize",
         "pad",
         "partition",
+        ("removeprefix", "1.4"),
+        ("removesuffix", "1.4"),
         "repeat",
         "replace",
         "rfind",


Test patch:
diff --git a/dask/dataframe/tests/test_accessors.py b/dask/dataframe/tests/test_accessors.py
--- a/dask/dataframe/tests/test_accessors.py
+++ b/dask/dataframe/tests/test_accessors.py
@@ -5,6 +5,7 @@
 
 pd = pytest.importorskip("pandas")
 import dask.dataframe as dd
+from dask.dataframe._compat import PANDAS_GT_140
 from dask.dataframe.utils import assert_eq
 
 
@@ -198,6 +199,22 @@ def test_str_accessor_extractall(df_ddf):
     )
 
 
+@pytest.mark.skipif(not PANDAS_GT_140, reason="requires pandas >= 1.4.0")
+@pytest.mark.parametrize("method", ["removeprefix", "removesuffix"])
+def test_str_accessor_removeprefix_removesuffix(df_ddf, method):
+    df, ddf = df_ddf
+    prefix = df.str_col.iloc[0][:2]
+    suffix = df.str_col.iloc[0][-2:]
+    missing = "definitely a missing prefix/suffix"
+
+    def call(df, arg):
+        return getattr(df.str_col.str, method)(arg)
+
+    assert_eq(call(ddf, prefix), call(df, prefix))
+    assert_eq(call(ddf, suffix), call(df, suffix))
+    assert_eq(call(ddf, missing), call(df, missing))
+
+
 def test_str_accessor_cat(df_ddf):
     df, ddf = df_ddf
     sol = df.str_col.str.cat(df.str_col.str.upper(), sep=":")