Skip to content

BUG: raise when sort_index with ascending=None #39451

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 26, 2021
2 changes: 1 addition & 1 deletion pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -5560,7 +5560,7 @@ def sort_index(
self,
axis: Axis = 0,
level: Optional[Level] = None,
ascending: bool = True,
ascending: Union[Union[bool, int], Sequence[Union[bool, int]]] = True,
inplace: bool = False,
kind: str = "quicksort",
na_position: str = "last",
Expand Down
10 changes: 8 additions & 2 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@
from pandas.compat.numpy import function as nv
from pandas.errors import AbstractMethodError, InvalidIndexError
from pandas.util._decorators import doc, rewrite_axis_style_signature
from pandas.util._validators import validate_bool_kwarg, validate_fillna_kwargs
from pandas.util._validators import (
validate_ascending,
validate_bool_kwarg,
validate_fillna_kwargs,
)

from pandas.core.dtypes.common import (
ensure_int64,
Expand Down Expand Up @@ -4522,7 +4526,7 @@ def sort_index(
self,
axis=0,
level=None,
ascending: bool_t = True,
ascending: Union[Union[bool_t, int], Sequence[Union[bool_t, int]]] = True,
inplace: bool_t = False,
kind: str = "quicksort",
na_position: str = "last",
Expand All @@ -4533,6 +4537,8 @@ def sort_index(

inplace = validate_bool_kwarg(inplace, "inplace")
axis = self._get_axis_number(axis)
ascending = validate_ascending(ascending)

target = self._get_axis(axis)

indexer = get_indexer_indexer(
Expand Down
9 changes: 6 additions & 3 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
Iterable,
List,
Optional,
Sequence,
Tuple,
Type,
Union,
cast,
)
import warnings

Expand Down Expand Up @@ -3039,7 +3041,7 @@ def update(self, other) -> None:
def sort_values(
self,
axis=0,
ascending=True,
ascending: Union[Union[bool, int], Sequence[Union[bool, int]]] = True,
inplace: bool = False,
kind: str = "quicksort",
na_position: str = "last",
Expand Down Expand Up @@ -3217,6 +3219,7 @@ def sort_values(
)

if is_list_like(ascending):
ascending = cast(Sequence[Union[bool, int]], ascending)
if len(ascending) != 1:
raise ValueError(
f"Length of ascending ({len(ascending)}) must be 1 for Series"
Expand All @@ -3231,7 +3234,7 @@ def sort_values(

# GH 35922. Make sorting stable by leveraging nargsort
values_to_sort = ensure_key_mapped(self, key)._values if key else self._values
sorted_index = nargsort(values_to_sort, kind, ascending, na_position)
sorted_index = nargsort(values_to_sort, kind, bool(ascending), na_position)

result = self._constructor(
self._values[sorted_index], index=self.index[sorted_index]
Expand All @@ -3249,7 +3252,7 @@ def sort_index(
self,
axis=0,
level=None,
ascending: bool = True,
ascending: Union[Union[bool, int], Sequence[Union[bool, int]]] = True,
inplace: bool = False,
kind: str = "quicksort",
na_position: str = "last",
Expand Down
3 changes: 2 additions & 1 deletion pandas/core/sorting.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Iterable,
List,
Optional,
Sequence,
Tuple,
Union,
)
Expand Down Expand Up @@ -41,7 +42,7 @@
def get_indexer_indexer(
target: Index,
level: Union[str, int, List[str], List[int]],
ascending: bool,
ascending: Union[Sequence[Union[bool, int]], Union[bool, int]],
kind: str,
na_position: str,
sort_remaining: bool,
Expand Down
16 changes: 16 additions & 0 deletions pandas/tests/frame/methods/test_sort_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,22 @@ def test_sort_index_with_categories(self, categories):
)
tm.assert_frame_equal(result, expected)

@pytest.mark.parametrize(
"ascending",
[
None,
(True, None),
(False, "True"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make these lists

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and maybe add an empty list too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently an empty list does not raise an error, so the test fails.
Should I change the code, so that an empty list raises?

],
)
def test_sort_index_ascending_bad_value_raises(self, ascending):
df = DataFrame(np.arange(64))
length = len(df.index)
df.index = [(i - length / 2) % length for i in range(length)]
match = 'For argument "ascending" expected type bool'
with pytest.raises(ValueError, match=match):
df.sort_index(axis=0, ascending=ascending, na_position="first")


class TestDataFrameSortIndexKey:
def test_sort_multi_index_key(self):
Expand Down
14 changes: 14 additions & 0 deletions pandas/tests/series/methods/test_sort_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,20 @@ def test_sort_index_ascending_list(self):
expected = ser.iloc[[0, 4, 1, 5, 2, 6, 3, 7]]
tm.assert_series_equal(result, expected)

@pytest.mark.parametrize(
"ascending",
[
None,
(True, None),
(False, "True"),
],
)
def test_sort_index_ascending_bad_value_raises(self, ascending):
ser = Series(range(10), index=[0, 3, 2, 1, 4, 5, 7, 6, 8, 9])
match = 'For argument "ascending" expected type bool'
with pytest.raises(ValueError, match=match):
ser.sort_index(ascending=ascending)


class TestSeriesSortIndexKey:
def test_sort_index_multiindex_key(self):
Expand Down
49 changes: 45 additions & 4 deletions pandas/util/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Module that contains many useful utilities
for validating data or function arguments
"""
from typing import Iterable, Union
from typing import Iterable, Sequence, Union
import warnings

import numpy as np
Expand Down Expand Up @@ -205,9 +205,39 @@ def validate_args_and_kwargs(fname, args, kwargs, max_fname_arg_count, compat_ar
validate_kwargs(fname, kwargs, compat_args)


def validate_bool_kwarg(value, arg_name):
""" Ensures that argument passed in arg_name is of type bool. """
if not (is_bool(value) or value is None):
def validate_bool_kwarg(value, arg_name, none_allowed=True, int_allowed=False):
"""
Ensure that argument passed in arg_name can be interpreted as boolean.

Parameters
----------
value : bool
Value to be validated.
arg_name : str
Name of the argument. To be reflected in the error message.
none_allowed : bool, default True
Whether to consider None to be a valid boolean.
int_allowed : bool, default False
Whether to consider integer value to be a valid boolean.

Returns
-------
value
The same value as input.

Raises
------
ValueError
If the value is not a valid boolean.
"""
good_value = is_bool(value)
if none_allowed:
good_value = good_value or value is None

if int_allowed:
good_value = good_value or isinstance(value, int)

if not good_value:
raise ValueError(
f'For argument "{arg_name}" expected type bool, received '
f"type {type(value).__name__}."
Expand Down Expand Up @@ -381,3 +411,14 @@ def validate_percentile(q: Union[float, Iterable[float]]) -> np.ndarray:
if not all(0 <= qs <= 1 for qs in q_arr):
raise ValueError(msg.format(q_arr / 100.0))
return q_arr


def validate_ascending(
ascending: Union[Union[bool, int], Sequence[Union[bool, int]]] = True,
):
"""Validate ``ascending`` kwargs for ``sort_index`` method."""
kwargs = {"none_allowed": False, "int_allowed": True}
if not isinstance(ascending, (list, tuple)):
return validate_bool_kwarg(ascending, "ascending", **kwargs)

return [validate_bool_kwarg(item, "ascending", **kwargs) for item in ascending]