Repository: pyjanitor-devs/pyjanitor (python)

Issue:
Deprecate janitor.patterns
janitor.patterns is not really useful (beauty of hindsight :grin: ) , as it is simply a wrapper around `re.compile`, which a user can import and wrap

```py
from re import compile as patterns # or whatever name user prefers
import janitor
import pandas as pd

df.select_columns(patterns('\d+')
```

Pull request description:
Deprecate `patterns`

# PR Description

Please describe the changes proposed in the pull request:

- Deprecate `patterns` function
- Minor refactor to `select_columns`


**This PR resolves #1007 .**

<!-- As you go down the PR template, please feel free to delete sections that are irrelevant. -->

# PR Checklist

<!-- This checklist exists for newcomers who are not yet familiar with our requirements. If you are experienced with
the project, please feel free to delete this section. -->

Please ensure that you have done the following:

1. [ ] PR in from a fork off your branch. Do not PR from `<your_username>`:`dev`, but rather from `<your_username>`:`<feature-branch_name>`.
<!-- Doing this helps us keep the commit history much cleaner than it would otherwise be. -->
2. [ ] If you're not on the contributors list, add yourself to `AUTHORS.rst`.
<!-- We'd like to acknowledge your contributions! -->
3. [ ] Add a line to `CHANGELOG.md` under the latest version header (i.e. the one that is "on deck") describing the contribution.
    - Do use some discretion here; if there are multiple PRs that are related, keep them in a single line.

# Automatic checks

There will be automatic checks run on the PR. These include:

- Building a preview of the docs on Netlify
- Automatically linting the code
- Making sure the code is documented
- Making sure that all tests are passed
- Making sure that code coverage doesn't go down.

# Relevant Reviewers

<!-- Finally, please tag relevant maintainers to review. -->

Please tag maintainers to review.

- @ericmjl


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84ea52b..5dcbd00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
 -   [BUG] `sort_column_value_order` no longer mutates original dataframe.
 -   [BUG] Extend `fill_empty`'s `column_names` type range. Issue #998. @Zeroto521
 -   [BUG] Removed/updated error-inducing default arguments in `row_to_names` (#1004) and `round_to_fraction` (#1005). @thatlittleboy
+-   [ENH] `patterns` deprecated in favour of importing `re.compile`. #1007 @samukweku
 
 ## [v0.22.0] - 2021-11-21
 
diff --git a/janitor/functions/utils.py b/janitor/functions/utils.py
index 305a1db..08dbf4d 100644
--- a/janitor/functions/utils.py
+++ b/janitor/functions/utils.py
@@ -1,6 +1,7 @@
 """Utility functions for all of the functions submodule."""
 from itertools import chain
 import fnmatch
+import warnings
 
 from collections.abc import Callable as dispatch_callable
 import re
@@ -119,11 +120,20 @@ def patterns(regex_pattern: Union[str, Pattern]) -> Pattern:
     it can be used to select columns in the index or columns_names
     arguments of `pivot_longer` function.
 
+    **Warning**:
+
+        This function is deprecated. Kindly use `re.compile` instead.
+
     :param regex_pattern: string to be converted to compiled regular
         expression.
     :returns: A compile regular expression from provided
         `regex_pattern`.
     """
+    warnings.warn(
+        "This function is deprecated. Kindly use `re.compile` instead.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
     check("regular expression", regex_pattern, [str, Pattern])
 
     return re.compile(regex_pattern)
@@ -262,18 +272,16 @@ def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
     Applies only to list type.
     It can take any of slice, str, callable, re.Pattern types,
     or a combination of these types.
-    A tuple of column names is returned.
+    A list of column names is returned.
     """
 
     # takes care of boolean entries
     if all(map(pd.api.types.is_bool, columns_to_select)):
         if len(columns_to_select) != len(df.columns):
             raise ValueError(
-                """
-                The length of the list of booleans
-                does not match the number of columns
-                in the dataframe.
-                """
+                "The length of the list of booleans "
+                "does not match the number of columns "
+                "in the dataframe."
             )
 
         return [*df.columns[columns_to_select]]
@@ -310,17 +318,14 @@ def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
     specifically, the `*`.
     A list of column names is returned.
     """
-    filtered_columns = None
-    df_columns = df.columns
+
     if "*" in columns_to_select:  # shell-style glob string (e.g., `*_thing_*`)
-        filtered_columns = fnmatch.filter(df_columns, columns_to_select)
-    elif columns_to_select in df_columns:
-        filtered_columns = [columns_to_select]
-        return filtered_columns
-    if not filtered_columns:
-        raise KeyError(f"No match was returned for '{columns_to_select}'")
-    df_columns = None
-    return filtered_columns
+        filtered_columns = fnmatch.filter(df.columns, columns_to_select)
+        if filtered_columns:
+            return filtered_columns
+    elif columns_to_select in df.columns:
+        return [columns_to_select]
+    raise KeyError(f"No match was returned for '{columns_to_select}'")
 
 
 @_select_column_names.register(slice)  # noqa: F811
@@ -345,11 +350,9 @@ def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
 
     if not df_columns.is_unique:
         raise ValueError(
-            """
-            The column labels are not unique.
-            Kindly ensure the labels are unique
-            to ensure the correct output.
-            """
+            "The column labels are not unique. "
+            "Kindly ensure the labels are unique "
+            "to ensure the correct output."
         )
 
     start, stop, step = (
@@ -362,40 +365,30 @@ def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
     step_check = any((step is None, isinstance(step, int)))
     if not start_check:
         raise ValueError(
-            """
-            The start value for the slice
-            must either be a string or `None`.
-            """
+            "The start value for the slice "
+            "must either be a string or `None`."
         )
     if not stop_check:
         raise ValueError(
-            """
-            The stop value for the slice
-            must either be a string or `None`.
-            """
+            "The stop value for the slice "
+            "must either be a string or `None`."
         )
     if not step_check:
         raise ValueError(
-            """
-            The step value for the slice
-            must either be an integer or `None`.
-            """
+            "The step value for the slice "
+            "must either be an integer or `None`."
         )
     start_check = any((start is None, start in df_columns))
     stop_check = any((stop is None, stop in df_columns))
     if not start_check:
         raise ValueError(
-            """
-            The start value for the slice must either be `None`
-            or exist in the dataframe's columns.
-            """
+            "The start value for the slice must either be `None` "
+            "or exist in the dataframe's columns."
         )
     if not stop_check:
         raise ValueError(
-            """
-            The stop value for the slice must either be `None`
-            or exist in the dataframe's columns.
-            """
+            "The stop value for the slice must either be `None` "
+            "or exist in the dataframe's columns."
         )
 
     if start is None:
@@ -435,10 +428,6 @@ def _column_sel_dispatch(columns_to_select, df):  # noqa: F811
     filtered_columns = df.agg(columns_to_select)
 
     if not filtered_columns.any():
-        raise ValueError(
-            """
-            No match was returned for the provided callable.
-            """
-        )
+        raise ValueError("No match was returned for the provided callable.")
 
     return [*df.columns[filtered_columns]]


Test patch:
diff --git a/tests/functions/test_pivot_longer.py b/tests/functions/test_pivot_longer.py
index 8250c1e..d4ba9c7 100644
--- a/tests/functions/test_pivot_longer.py
+++ b/tests/functions/test_pivot_longer.py
@@ -3,7 +3,7 @@ import pandas as pd
 import pytest
 from pandas.testing import assert_frame_equal
 
-from janitor import patterns
+import re
 
 
 @pytest.fixture
@@ -437,7 +437,7 @@ def test_pivot_index_patterns_only_sort_by_appearance(
     and `sort_by_appearance` is `True`.
     """
     result = df_checks.pivot_longer(
-        index=patterns(r"[^\d+]"),
+        index=re.compile(r"[^\d+]"),
         names_to="year",
         values_to="num_nests",
         sort_by_appearance=True,
@@ -463,7 +463,7 @@ def test_pivot_no_index_no_columns():
 def test_pivot_columns_patterns_only(df_checks, df_checks_output):
     """Test output if the `patterns` function is passed to `column_names`."""
     result = df_checks.pivot_longer(
-        column_names=patterns(r"\d+"),
+        column_names=re.compile(r"\d+"),
         names_to="year",
         values_to="num_nests",
         sort_by_appearance=True,
@@ -1166,7 +1166,7 @@ paired_columns_pattern = [
                 },
             ]
         ),
-        patterns("^(?!(date|val))"),
+        re.compile("^(?!(date|val))"),
         (".value", "value"),
         r"([a-z]+)(\d)",
         True,
@@ -1944,7 +1944,7 @@ single_column_names_pattern = [
             ]
         ),
         "species",
-        patterns("_"),
+        re.compile("_"),
         "part",
         r"(.*)[_].*",
         True,
diff --git a/tests/utils/test__select_column.py b/tests/utils/test__select_column.py
index 6385220..c9b3949 100644
--- a/tests/utils/test__select_column.py
+++ b/tests/utils/test__select_column.py
@@ -5,8 +5,7 @@ import numpy as np
 import pandas as pd
 import pytest
 
-from janitor import patterns
-from janitor.functions.utils import _select_column_names
+from janitor.functions.utils import _select_column_names, patterns
 
 
 @pytest.fixture
@@ -158,6 +157,15 @@ def test_slice_presence(df):
         _select_column_names(slice("id", "M_end_date"), df)
 
 
+def test_slice_unique():
+    """
+    Raise ValueError if the columns are not unique.
+    """
+    not_unique = pd.DataFrame([], columns=["code", "code", "code1", "code2"])
+    with pytest.raises(ValueError):
+        _select_column_names(slice("code", "code2"), not_unique)
+
+
 def test_callable(df):
     """
     Check that error is raised if `columns_to_select` is a
@@ -168,6 +176,16 @@ def test_callable(df):
         _select_column_names(object, df)
 
 
+def test_patterns_warning(df1):
+    """
+    Check that warning is raised if `janitor.patterns` is used.
+    """
+    with pytest.warns(DeprecationWarning):
+        assert _select_column_names(patterns(r"\d$"), df1) == list(
+            df1.filter(regex=r"\d$").columns
+        )
+
+
 @pytest.mark.xfail(reason="Indexing in Pandas is possible with a Series.")
 def test_callable_returns_Series(df):
     """
@@ -241,6 +259,11 @@ def test_slice(df1):
         _select_column_names(slice(None, None, 2), df1)
         == df1.loc[:, slice(None, None, 2)].columns.tolist()
     )
+    assert _select_column_names(slice("code2", "code"), df1) == [
+        "code2",
+        "code1",
+        "code",
+    ]
 
 
 def test_callable_data_type(df1):
@@ -320,9 +343,6 @@ def test_regex(df1):
     assert _select_column_names(re.compile(r"\d$"), df1) == list(
         df1.filter(regex=r"\d$").columns
     )
-    assert _select_column_names(patterns(r"\d$"), df1) == list(
-        df1.filter(regex=r"\d$").columns
-    )
 
 
 def test_tuple(df_tuple):