Repository: pandas-dev/pandas

Issue:
Styler class should have a `pipe()` method, akin to DataFrame.pipe()
#### Problem description

Users may want to create functions that apply customized styling to a dataframe or styler object.

In order to make these functions composable, a convenient signature is `f(styler) -> Styler`, so that a user can chain multiple such functions together, in a "mix-and-match" fashion.  (N.B. Such functions can obtain the underlying dataframe using the `styler.data` attribute.)

A method `Styler.pipe()` would provide an ergonomic way to apply these functions sequentially, after a series of Dataframe-related manipulations.

Usage example:
```
def right_align(styler):
    return styler.set_properties(**{'text-align': 'right'})

def highlight_rows_gt(styler, colname, threshold):
    return ...        # implementation omitted

>>> (df.groupby('A')[['B', 'C']].sum()
...        .assign(D=lambda x: x['B'] / x['C'])
...        .style
...        .format('{:.2f}')
...        .pipe(highlight_rows_gt, colname='D', threshold=0.95)
...        .pipe(right_align))
```


Discussion:

Resolving patch:
diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py
--- a/pandas/io/formats/style.py
+++ b/pandas/io/formats/style.py
@@ -1235,6 +1235,75 @@ class MyStyler(cls):
 
         return MyStyler
 
+    def pipe(self, func, *args, **kwargs):
+        """
+        Apply ``func(self, *args, **kwargs)``, and return the result.
+
+        .. versionadded:: 0.24.0
+
+        Parameters
+        ----------
+        func : function
+            Function to apply to the Styler.  Alternatively, a
+            ``(callable, keyword)`` tuple where ``keyword`` is a string
+            indicating the keyword of ``callable`` that expects the Styler.
+        *args, **kwargs :
+            Arguments passed to `func`.
+
+        Returns
+        -------
+        object :
+            The value returned by ``func``.
+
+        See Also
+        --------
+        DataFrame.pipe : Analogous method for DataFrame.
+        Styler.apply : Apply a function row-wise, column-wise, or table-wise to
+            modify the dataframe's styling.
+
+        Notes
+        -----
+        Like :meth:`DataFrame.pipe`, this method can simplify the
+        application of several user-defined functions to a styler.  Instead
+        of writing:
+
+        .. code-block:: python
+
+            f(g(df.style.set_precision(3), arg1=a), arg2=b, arg3=c)
+
+        users can write:
+
+        .. code-block:: python
+
+            (df.style.set_precision(3)
+               .pipe(g, arg1=a)
+               .pipe(f, arg2=b, arg3=c))
+
+        In particular, this allows users to define functions that take a
+        styler object, along with other parameters, and return the styler after
+        making styling changes (such as calling :meth:`Styler.apply` or
+        :meth:`Styler.set_properties`).  Using ``.pipe``, these user-defined
+        style "transformations" can be interleaved with calls to the built-in
+        Styler interface.
+
+        Examples
+        --------
+        >>> def format_conversion(styler):
+        ...     return (styler.set_properties(**{'text-align': 'right'})
+        ...                   .format({'conversion': '{:.1%}'}))
+
+        The user-defined ``format_conversion`` function above can be called
+        within a sequence of other style modifications:
+
+        >>> df = pd.DataFrame({'trial': list(range(5)),
+        ...                    'conversion': [0.75, 0.85, np.nan, 0.7, 0.72]})
+        >>> (df.style
+        ...    .highlight_min(subset=['conversion'], color='yellow')
+        ...    .pipe(format_conversion)
+        ...    .set_caption("Results with minimum conversion highlighted."))
+        """
+        return com._pipe(self, func, *args, **kwargs)
+
 
 def _is_visible(idx_row, idx_col, lengths):
     """


Test patch:
diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py
--- a/pandas/tests/io/formats/test_style.py
+++ b/pandas/tests/io/formats/test_style.py
@@ -1173,6 +1173,22 @@ def test_hide_columns_mult_levels(self):
         assert ctx['body'][1][2]['is_visible']
         assert ctx['body'][1][2]['display_value'] == 3
 
+    def test_pipe(self):
+        def set_caption_from_template(styler, a, b):
+            return styler.set_caption(
+                'Dataframe with a = {a} and b = {b}'.format(a=a, b=b))
+
+        styler = self.df.style.pipe(set_caption_from_template, 'A', b='B')
+        assert 'Dataframe with a = A and b = B' in styler.render()
+
+        # Test with an argument that is a (callable, keyword_name) pair.
+        def f(a, b, styler):
+            return (a, b, styler)
+
+        styler = self.df.style
+        result = styler.pipe((f, 'styler'), a=1, b=2)
+        assert result == (1, 2, styler)
+
 
 @td.skip_if_no_mpl
 class TestStylerMatplotlibDep(object):