Repository: pyjanitor-devs/pyjanitor

Issue:
[ENH] Move `_` private functions in functions.py to utils.py
File is so long right now...


Discussion:

Resolving patch:
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -13,42 +13,16 @@
 from sklearn.preprocessing import LabelEncoder
 from typing import Callable, Dict, Iterable, List, Union, Any, Tuple
 
+from .utils import (
+    deprecated_alias,
+    _strip_underscores,
+    check,
+    _clean_accounting_column,
+    _currency_column_to_numeric,
+    _replace_empty_string_with_none,
+    _replace_original_empty_string_with_none,
+)
 from .errors import JanitorError
-from .utils import deprecated_alias
-
-
-def _strip_underscores(
-    df: pd.DataFrame, strip_underscores: Union[str, bool] = None
-) -> pd.DataFrame:
-    """
-    Strip underscores from DataFrames column names.
-
-    Underscores can be stripped from the beginning, end or both.
-
-    .. code-block:: python
-
-        df = _strip_underscores(df, strip_underscores='left')
-
-    :param df: The pandas DataFrame object.
-    :param strip_underscores: (optional) Removes the outer underscores from all
-        column names. Default None keeps outer underscores. Values can be
-        either 'left', 'right' or 'both' or the respective shorthand 'l', 'r'
-        and True.
-    :returns: A pandas DataFrame with underscores removed.
-    """
-    underscore_options = [None, "left", "right", "both", "l", "r", True]
-    if strip_underscores not in underscore_options:
-        raise JanitorError(
-            f"strip_underscores must be one of: {underscore_options}"
-        )
-
-    if strip_underscores in ["left", "l"]:
-        df = df.rename(columns=lambda x: x.lstrip("_"))
-    elif strip_underscores in ["right", "r"]:
-        df = df.rename(columns=lambda x: x.rstrip("_"))
-    elif strip_underscores == "both" or strip_underscores is True:
-        df = df.rename(columns=lambda x: x.strip("_"))
-    return df
 
 
 @pf.register_dataframe_method
@@ -2238,110 +2212,6 @@ def reset_index_inplace(df: pd.DataFrame, *args, **kwargs) -> pd.DataFrame:
     return df
 
 
-def check(varname: str, value, expected_types: list):
-    """
-    One-liner syntactic sugar for checking types.
-
-    Should be used like this::
-
-        check('x', x, [int, float])
-
-    :param varname: The name of the variable.
-    :param value: The value of the varname.
-    :param expected_types: The types we expect the item to be.
-    :returns: TypeError if data is not the expected type.
-    """
-    is_expected_type = False
-    for t in expected_types:
-        if isinstance(value, t):
-            is_expected_type = True
-            break
-
-    if not is_expected_type:
-        raise TypeError(
-            "{varname} should be one of {expected_types}".format(
-                varname=varname, expected_types=expected_types
-            )
-        )
-
-
-def _clean_accounting_column(x: str) -> float:
-    """
-    Perform the logic for the `cleaning_style == "accounting"` attribute.
-
-    This is a private function, not intended to be used outside of
-    ``currency_column_to_numeric``.
-
-    It is intended to be used in a pandas `apply` method.
-
-    :returns: An object with a cleaned column.
-    """
-    y = x.strip()
-    y = y.replace(",", "")
-    y = y.replace(")", "")
-    y = y.replace("(", "-")
-    if y == "-":
-        return 0.00
-    return float(y)
-
-
-def _currency_column_to_numeric(x, cast_non_numeric=None) -> str:
-    """
-    Perform logic for changing cell values.
-
-    This is a private function intended to be used only in
-    ``currency_column_to_numeric``.
-
-    It is intended to be used in a pandas `apply` method, after being passed
-    through `partial`.
-    """
-    acceptable_currency_characters = {
-        "-",
-        ".",
-        "1",
-        "2",
-        "3",
-        "4",
-        "5",
-        "6",
-        "7",
-        "8",
-        "9",
-        "0",
-    }
-    if len(x) == 0:
-        return "ORIGINAL_NA"
-
-    if cast_non_numeric:
-        if x in cast_non_numeric.keys():
-            check(
-                "{%r: %r}" % (x, str(cast_non_numeric[x])),
-                cast_non_numeric[x],
-                [int, float],
-            )
-            return cast_non_numeric[x]
-        else:
-            return "".join(i for i in x if i in acceptable_currency_characters)
-    else:
-        return "".join(i for i in x if i in acceptable_currency_characters)
-
-
-def _replace_empty_string_with_none(x):
-    if isinstance(x, int):
-        return x
-
-    elif isinstance(x, float):
-        return x
-
-    elif len(x):
-        return x
-
-
-def _replace_original_empty_string_with_none(x):
-    if x != "ORIGINAL_NA":
-        return x
-
-
 @pf.register_dataframe_method
 @deprecated_alias(col_name="column_name", type="cleaning_style")
 def currency_column_to_numeric(
diff --git a/janitor/utils.py b/janitor/utils.py
--- a/janitor/utils.py
+++ b/janitor/utils.py
@@ -4,13 +4,163 @@
 import warnings
 import pandas as pd
 
-from typing import Callable
+from typing import Callable, Dict, Union
+from .errors import JanitorError
 
 
-def import_message(submodule, package, installation):
+def check(varname: str, value, expected_types: list):
+    """
+    One-liner syntactic sugar for checking types.
+
+    Should be used like this::
+
+        check('x', x, [int, float])
+
+    :param varname: The name of the variable.
+    :param value: The value of the varname.
+    :param expected_types: The types we expect the item to be.
+    :returns: TypeError if data is not the expected type.
+    """
+    is_expected_type = False
+    for t in expected_types:
+        if isinstance(value, t):
+            is_expected_type = True
+            break
+
+    if not is_expected_type:
+        raise TypeError(
+            "{varname} should be one of {expected_types}".format(
+                varname=varname, expected_types=expected_types
+            )
+        )
+
+
+def _clean_accounting_column(x: str) -> float:
+    """
+    Perform the logic for the `cleaning_style == "accounting"` attribute.
+
+    This is a private function, not intended to be used outside of
+    ``currency_column_to_numeric``.
+
+    It is intended to be used in a pandas `apply` method.
+
+    :returns: An object with a cleaned column.
+    """
+    y = x.strip()
+    y = y.replace(",", "")
+    y = y.replace(")", "")
+    y = y.replace("(", "-")
+    if y == "-":
+        return 0.00
+    return float(y)
+
+
+def _currency_column_to_numeric(x, cast_non_numeric=None) -> str:
+    """
+    Perform logic for changing cell values.
+
+    This is a private function intended to be used only in
+    ``currency_column_to_numeric``.
+
+    It is intended to be used in a pandas `apply` method, after being passed
+    through `partial`.
+    """
+    acceptable_currency_characters = {
+        "-",
+        ".",
+        "1",
+        "2",
+        "3",
+        "4",
+        "5",
+        "6",
+        "7",
+        "8",
+        "9",
+        "0",
+    }
+    if len(x) == 0:
+        return "ORIGINAL_NA"
+
+    if cast_non_numeric:
+        if x in cast_non_numeric.keys():
+            check(
+                "{%r: %r}" % (x, str(cast_non_numeric[x])),
+                cast_non_numeric[x],
+                [int, float],
+            )
+            return cast_non_numeric[x]
+        else:
+            return "".join(i for i in x if i in acceptable_currency_characters)
+    else:
+        return "".join(i for i in x if i in acceptable_currency_characters)
+
+
+def _replace_empty_string_with_none(x):
+    if isinstance(x, int):
+        return x
+
+    elif isinstance(x, float):
+        return x
+
+    elif len(x):
+        return x
+
+
+def _replace_original_empty_string_with_none(x):
+    if x != "ORIGINAL_NA":
+        return x
+
+
+def _strip_underscores(
+    df: pd.DataFrame, strip_underscores: Union[str, bool] = None
+) -> pd.DataFrame:
+    """
+    Strip underscores from DataFrames column names.
+
+    Underscores can be stripped from the beginning, end or both.
+
+    .. code-block:: python
+
+        df = _strip_underscores(df, strip_underscores='left')
+
+    :param df: The pandas DataFrame object.
+    :param strip_underscores: (optional) Removes the outer underscores from all
+        column names. Default None keeps outer underscores. Values can be
+        either 'left', 'right' or 'both' or the respective shorthand 'l', 'r'
+        and True.
+    :returns: A pandas DataFrame with underscores removed.
+    """
+    underscore_options = [None, "left", "right", "both", "l", "r", True]
+    if strip_underscores not in underscore_options:
+        raise JanitorError(
+            f"strip_underscores must be one of: {underscore_options}"
+        )
+
+    if strip_underscores in ["left", "l"]:
+        df = df.rename(columns=lambda x: x.lstrip("_"))
+    elif strip_underscores in ["right", "r"]:
+        df = df.rename(columns=lambda x: x.rstrip("_"))
+    elif strip_underscores == "both" or strip_underscores is True:
+        df = df.rename(columns=lambda x: x.strip("_"))
+    return df
+
+
+def import_message(submodule: str, package: str, installation: str):
+    """
+    Generic message for indicating to the user when a function relies on an
+    optional module / package that is not currently installed. Includes
+    installation instructions. Used in `chemistry.py` and `biology.py`.
+
+    :param submodule: pyjanitor submodule that needs an external dependency.
+    :param package: External package this submodule relies on.
+    :param installation: Command to execute in the environment to install
+        the package.
+    """
+
     print(
-        f"To use the janitor submodule {submodule}, you need to install \
-        {package}."
+        f"To use the janitor submodule {submodule}, you need to install "
+        f"{package}."
     )
     print()
     print(f"To do so, use the following command:")
@@ -20,21 +170,25 @@ def import_message(submodule, package, installation):
 
 def idempotent(func: Callable, df: pd.DataFrame, *args, **kwargs):
     """
-    Checks if a function is idempotent,
-    that is, f(f(x))=f(x) is true for all x.
-
-    :param func: a python method
-    :param df: a pandas dataframe
-    :param args: Arguments supplied to the method
-    :param kwargs: Arguments supplied to the method
-    :return:
+    Raises error if a function operating on a `DataFrame` is not idempotent,
+    that is, `func(func(df)) = func(df)` is not true for all `df`.
+
+    :param func: A python method.
+    :param df: A pandas `DataFrame`.
+    :param args: Positional arguments supplied to the method.
+    :param kwargs: Keyword arguments supplied to the method.
+    :raises ValueError: If `func` is found to not be idempotent for the given
+        `DataFrame` `df`.
     """
-    assert func(df, *args, **kwargs) == func(
+    if not func(df, *args, **kwargs) == func(
         func(df, *args, **kwargs), *args, **kwargs
-    )
+    ):
+        raise ValueError(
+            "Supplied function is not idempotent for the given " "DataFrame."
+        )
 
 
-def deprecated_alias(**aliases):
+def deprecated_alias(**aliases) -> Callable:
     """
     Used as a decorator when deprecating old function argument names, while
     keeping backwards compatibility.
@@ -51,8 +205,9 @@ def deprecated_alias(**aliases):
         def simple_sum(alpha, beta):
             return alpha + beta
 
-    :param aliases: dictionary of aliases for a function's arguments
-    :return:
+    :param aliases: Dictionary of aliases for a function's arguments.
+    :return: Your original function wrapped with the kwarg redirection
+        function.
     """
 
     def decorator(func):
@@ -66,19 +221,19 @@ def wrapper(*args, **kwargs):
     return decorator
 
 
-def rename_kwargs(func_name, kwargs, aliases):
+def rename_kwargs(func_name: str, kwargs: Dict, aliases: Dict):
     """
-    Used to update deprecated argument names with new names. Throws a TypeError if
-    both arguments are provided, and warns if old alias is used.
+    Used to update deprecated argument names with new names. Throws a
+    TypeError if both arguments are provided, and warns if old alias is used.
 
     Implementation is inspired from `StackOverflow`_.
 
     .. _StackOverflow: https://stackoverflow.com/questions/49802412/how-to-implement-deprecation-in-python-with-argument-alias  # noqa: E501
 
-    :param func_name: name of decorated function
-    :param aliases: dictionary of aliases for a function's arguments
-    :param kwargs: arguments supplied to the method
-    :return:
+    :param func_name: name of decorated function.
+    :param kwargs: Arguments supplied to the method.
+    :param aliases: Dictionary of aliases for a function's arguments.
+    :return: Nothing; the passed `kwargs` are modified directly.
     """
     for old_alias, new_alias in aliases.items():
         if old_alias in kwargs:


Test patch: