Repository: pyjanitor-devs/pyjanitor

Issue:
[DOC] #noqa comment appears in docs
# Brief Description of Fix

<!-- Please describe the fix in terms of a "before" and "after". In other words, what's not so good about the current docs
page, and what you would like to see it become. 

Example starter wording is provided. -->

A `#noqa:E501` comment in the docstring from `encode_categorical()` appears in [the docs](https://pyjanitor.readthedocs.io/reference/general_functions.html#preprocessing). The `#noqa:E501` comment exists due to a long url in the docstring which angers `pycodestyle`. 

I'm currently not familiar enough with `reStructuredText` or `Sphinx` to know how to solve this issue.

A quick search of the published docs shows [a number of #noqa comments](https://pyjanitor.readthedocs.io/search.html?q=%23noqa&check_keywords=yes&area=default) that may not be desired.

# Relevant Context

<!-- Please put here, in bullet points, links to the relevant docs page. A few starting template points are available
to get you started. -->

- [Link to documentation page](https://pyjanitor.readthedocs.io/reference/general_functions.html#preprocessing)
- [Link to exact file to be edited](https://github.com/ericmjl/pyjanitor/blob/dev/janitor/functions.py)



Discussion:
This is a great catch, @hectormz! I'm not too worried about these; the `# noqa` comments are in the docstrings, and as such they get linted. A way around them is to modify the code blocks, but that shouldn't be a big problem.

For now, I'll tag this as "needs further discussion", as I don't have strong opinions on whether they should appear in the docs or not - but I'd like this to continue to be discussed.
Sure, not mission critical. Just happened to notice it, and knew it wasn't our intent to have `#noqa` leak into docs.

On a related note, it looks like the url in [remove_empty doc](https://pyjanitor.readthedocs.io/reference/janitor.functions/janitor.remove_empty.html?highlight=stackoverflow#janitor-remove-empty) is formed correctly, while [convert_matlab_date](https://pyjanitor.readthedocs.io/reference/janitor.functions/janitor.convert_matlab_date.html?highlight=stack%20overflow#janitor-convert-matlab-date) is not.

That should be an easy fix
I spent a little time looking into this. A good number of `#noqa` in the docstrings come from lines with long urls. They're formatted like:

```

Implementation is inspired from `StackOverflow

<https://stackoverflow.com/questions/38884538/python-pandas-find-all-rows-where-all-values-are-nan>`_.  # noqa: E501

```

OR

```
Implementation is inspired from `StackOverflow`_.

    .. _StackOverflow: https://stackoverflow.com/questions/38884538/python-pandas-find-all-rows-where-all-values-are-nan  # noqa: E501
```

The first method will show `#noqa: E501` in the docs, while the second method will not. In reality, the second method appends the comment to the url:
```
https://stackoverflow.com/questions/38884538/python-pandas-find-all-rows-where-all-values-are-nan#noqa:E501
```
Assuming the url does not use `#noqa` itself to navigate to a certain part of the page, it navigates to the site as expected. This method hides `#noqa` in the docs, but is not technically _"right"_.

One final option (to my knowledge) would be to break up the url across multiple lines in the docstring, and avoid the use of `#noqa`. This works, but begs the question is the url/hyperlink more for the docs, or those looking directly at the code:

```
Implementation is inspired from `StackOverflow 

<https://stackoverflow.com/questions/38884538/
python-pandas-find-all-rows-where-all-values-are-nan>`_.
```

I had this problem in a package I maintain, and we solved it by moving `#noqa` to the line where the triple quote ends, like this:

```
"""A fancy docstring.

It'd be a shame if we ruined this beautiful docstring with a long
`StackOverflow`_ link.

.. _StackOverflow: https://stackoverflow.com/questions/38884538/python-pandas-find-all-rows-where-all-values-are-nan
"""  # noqa: E501
```

Example from my package's code: https://github.com/glotzerlab/freud/blob/0dae9cee1423ae9a0ed8923547e44c41b93b5672/freud/order.pyx#L188-L194
Thanks for the suggestion @bdice it worked nicely! @ericmjl , do you agree this is a good way forward, to apply #noqa E501 to docstrings? The only drawback is that all new content in the docstring gets a pass, but only hyperlinks should be allowed to be "too long".
@hectormz let's use the trick that @bdice suggested.

In the future, 
I am going to push for us to use [semantic line breaks](https://sembr.org) 
rather than 79-character line breaks. 
We get more lines of docs, 
but they are easier to diff and read.
:smile:


@ericmjl I see what you did there. 😆 Glad to see that worked for you. Good luck!

Resolving patch:
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -242,8 +242,8 @@ def _normalize_1(col_name: str) -> str:
 def _strip_accents(col_name: str) -> str:
     """
     Removes accents from a DataFrame column name.
-    .. _StackOverflow: https://stackoverflow.com/questions/517923/what-is-the-best-way-to-remove-accents-in-a-python-unicode-string # noqa: E501
-    """
+    .. _StackOverflow: https://stackoverflow.com/questions/517923/what-is-the-best-way-to-remove-accents-in-a-python-unicode-string
+    """  # noqa: E501
     return "".join(
         l
         for l in unicodedata.normalize("NFD", col_name)
@@ -263,7 +263,7 @@ def remove_empty(df: pd.DataFrame) -> pd.DataFrame:
 
     Implementation is inspired from `StackOverflow`_.
 
-    .. _StackOverflow: https://stackoverflow.com/questions/38884538/python-pandas-find-all-rows-where-all-values-are-nan  # noqa: E501
+    .. _StackOverflow: https://stackoverflow.com/questions/38884538/python-pandas-find-all-rows-where-all-values-are-nan
 
     Functional usage example:
 
@@ -282,7 +282,7 @@ def remove_empty(df: pd.DataFrame) -> pd.DataFrame:
     :param df: The pandas DataFrame object.
 
     :returns: A pandas DataFrame.
-    """
+    """  # noqa: E501
     nanrows = df.index[df.isnull().all(axis=1)]
     df = df.drop(index=nanrows).reset_index(drop=True)
 
@@ -335,7 +335,7 @@ def encode_categorical(
 ) -> pd.DataFrame:
     """
     Encode the specified columns with Pandas'
-    `category dtype <http://pandas.pydata.org/pandas-docs/stable/user_guide/categorical.html>`_. # noqa:E501
+    `category dtype <http://pandas.pydata.org/pandas-docs/stable/user_guide/categorical.html>`_.
 
     This method mutates the original DataFrame.
 
@@ -359,7 +359,7 @@ def encode_categorical(
     :param str/iterable column_names: A column name or an iterable (list or
         tuple) of column names.
     :returns: A pandas DataFrame
-    """
+    """  # noqa: E501
     if isinstance(column_names, list) or isinstance(column_names, tuple):
         for col in column_names:
             if col not in df.columns:
@@ -454,8 +454,7 @@ def rename_column(
 
         import pandas as pd
         import janitor
-        df = pd.DataFrame(...).rename_column("old_column_name",
-        "new_column_name")  # noqa: E501
+        df = pd.DataFrame(...).rename_column("old_column_name", "new_column_name")
 
     This is just syntactic sugar/a convenience function for renaming one column
     at a time. If you are convinced that there are multiple columns in need of
@@ -465,7 +464,7 @@ def rename_column(
     :param old_column_name: The old column name.
     :param new_column_name: The new column name.
     :returns: A pandas DataFrame with renamed columns.
-    """
+    """  # noqa: E501
     check_column(df, [old_column_name])
 
     return df.rename(columns={old_column_name: new_column_name})
@@ -488,8 +487,7 @@ def rename_columns(df: pd.DataFrame, new_column_names: Dict) -> pd.DataFrame:
 
         import pandas as pd
         import janitor
-        df = pd.DataFrame(...).rename_columns({"old_column_name":
-        "new_column_name"})  # noqa: E501
+        df = pd.DataFrame(...).rename_columns({"old_column_name": "new_column_name"})
 
     This is just syntactic sugar/a convenience function for renaming one column
     at a time. If you are convinced that there are multiple columns in need of
@@ -498,7 +496,7 @@ def rename_columns(df: pd.DataFrame, new_column_names: Dict) -> pd.DataFrame:
     :param df: The pandas DataFrame object.
     :param new_column_names: A dictionary of old and new column names.
     :returns: A pandas DataFrame with renamed columns.
-    """
+    """  # noqa: E501
     check_column(df, list(new_column_names.keys()))
 
     return df.rename(columns=new_column_names)
@@ -628,7 +626,7 @@ def convert_excel_date(df: pd.DataFrame, column_name) -> pd.DataFrame:
 
     Implementation is also from `Stack Overflow`.
 
-    .. _Stack Overflow: https://stackoverflow.com/questions/38454403/convert-excel-style-date-with-pandas  # noqa: E501
+    .. _Stack Overflow: https://stackoverflow.com/questions/38454403/convert-excel-style-date-with-pandas
 
     Functional usage example:
 
@@ -647,7 +645,7 @@ def convert_excel_date(df: pd.DataFrame, column_name) -> pd.DataFrame:
     :param df: A pandas DataFrame.
     :param str column_name: A column name.
     :returns: A pandas DataFrame with corrected dates.
-    """
+    """  # noqa: E501
     df[column_name] = pd.TimedeltaIndex(
         df[column_name], unit="d"
     ) + dt.datetime(
@@ -662,7 +660,9 @@ def convert_matlab_date(df: pd.DataFrame, column_name) -> pd.DataFrame:
     """
     Convert Matlab's serial date number into Python datetime format.
 
-    Implementation is also from `Stack Overflow <https://stackoverflow.com/questions/13965740/converting-matlabs-datenum-format-to-python>`.  # noqa: E501
+    Implementation is also from `StackOverflow`_.
+
+    .. _StackOverflow: https://stackoverflow.com/questions/13965740/converting-matlabs-datenum-format-to-python
 
     This method mutates the original DataFrame.
 
@@ -683,7 +683,7 @@ def convert_matlab_date(df: pd.DataFrame, column_name) -> pd.DataFrame:
     :param df: A pandas DataFrame.
     :param str column_name: A column name.
     :returns: A pandas DataFrame with corrected dates.
-    """
+    """  # noqa: E501
     days = pd.Series([dt.timedelta(v % 1) for v in df[column_name]])
     df[column_name] = (
         df[column_name].astype(int).apply(dt.datetime.fromordinal)
@@ -875,7 +875,7 @@ def concatenate_columns(
         else:
             df[new_column_name] = (
                 df[new_column_name] + sep + df[col].astype(str)
-            )  # noqa: E501
+            )
 
     return df
 
@@ -1037,8 +1037,7 @@ def filter_string(
     .. code-block:: python
 
         df = (pd.DataFrame(...)
-              .filter_string('column', search_string='pattern', c
-                                omplement=False)  # noqa: E501
+              .filter_string('column', search_string='pattern', complement=False)
               ...)  # chain on more data preprocessing.
 
     This stands in contrast to the in-place syntax that is usually used:
@@ -1075,7 +1074,7 @@ def filter_string(
     :param search_string: A regex pattern or a (sub-)string to search.
     :param complement: Whether to return the complement of the filter or not.
     :returns: A filtered pandas DataFrame.
-    """
+    """  # noqa: E501
     criteria = df[column_name].str.contains(search_string)
     if complement:
         return df[~criteria]
diff --git a/janitor/utils.py b/janitor/utils.py
--- a/janitor/utils.py
+++ b/janitor/utils.py
@@ -234,7 +234,7 @@ def deprecated_alias(**aliases) -> Callable:
 
     Implementation is inspired from `StackOverflow`_.
 
-    .. _StackOverflow: https://stackoverflow.com/questions/49802412/how-to-implement-deprecation-in-python-with-argument-alias  # noqa: E501
+    .. _StackOverflow: https://stackoverflow.com/questions/49802412/how-to-implement-deprecation-in-python-with-argument-alias
 
     Functional usage example:
 
@@ -247,7 +247,7 @@ def simple_sum(alpha, beta):
     :param aliases: Dictionary of aliases for a function's arguments.
     :return: Your original function wrapped with the kwarg redirection
         function.
-    """
+    """  # noqa: E501
 
     def decorator(func):
         @functools.wraps(func)
@@ -265,7 +265,7 @@ def refactored_function(message: str) -> Callable:
     
     Implementation is inspired from `Hacker Noon`_.
 
-    .. Hacker Noon: https://hackernoon.com/why-refactoring-how-to-restructure-python-package-51b89aa91987  # noqa: E501
+    .. Hacker Noon: https://hackernoon.com/why-refactoring-how-to-restructure-python-package-51b89aa91987
 
     Functional usage example:
 
@@ -280,7 +280,7 @@ def simple_sum(alpha, beta):
     :param message: Message to use in warning user about refactoring.
     :return: Your original function wrapped with the kwarg redirection
         function.
-    """
+    """  # noqa: E501
 
     def decorator(func):
         def emit_warning(*args, **kwargs):
@@ -299,13 +299,13 @@ def rename_kwargs(func_name: str, kwargs: Dict, aliases: Dict):
 
     Implementation is inspired from `StackOverflow`_.
 
-    .. _StackOverflow: https://stackoverflow.com/questions/49802412/how-to-implement-deprecation-in-python-with-argument-alias  # noqa: E501
+    .. _StackOverflow: https://stackoverflow.com/questions/49802412/how-to-implement-deprecation-in-python-with-argument-alias
 
     :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.
-    """
+    """  # noqa: E501
     for old_alias, new_alias in aliases.items():
         if old_alias in kwargs:
             if new_alias in kwargs:


Test patch: