Repository: pandas-dev/pandas
Pull request #57746 (2024-03-06 04:01:24)

CLN: Enforce deprecation of groupby.idxmin/idxmax with skipna=False not raising
- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.

Ref: #54234 

In the original deprecation, I missed the case of all NA values. Opened https://github.com/pandas-dev/pandas/issues/57745 to track.


Patch:
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 7802ef4798..a349f2287b 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -189,6 +189,7 @@ Other Deprecations
 
 Removal of prior version deprecations/changes
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+- :class:`.DataFrameGroupBy.idxmin`, :class:`.DataFrameGroupBy.idxmax`, :class:`.SeriesGroupBy.idxmin`, and :class:`.SeriesGroupBy.idxmax` will now raise a ``ValueError`` when used with ``skipna=False`` and an NA value is encountered (:issue:`10694`)
 - :func:`read_excel`, :func:`read_json`, :func:`read_html`, and :func:`read_xml` no longer accept raw string or byte representation of the data. That type of data must be wrapped in a :py:class:`StringIO` or :py:class:`BytesIO` (:issue:`53767`)
 - :meth:`Series.dt.to_pydatetime` now returns a :class:`Series` of :py:class:`datetime.datetime` objects (:issue:`52459`)
 - :meth:`SeriesGroupBy.agg` no longer pins the name of the group to the input passed to the provided ``func`` (:issue:`51703`)
@@ -245,6 +246,7 @@ Removal of prior version deprecations/changes
 
 Performance improvements
 ~~~~~~~~~~~~~~~~~~~~~~~~
+- :meth:`RangeIndex.append` returns a :class:`RangeIndex` instead of a :class:`Index` when appending values that could continue the :class:`RangeIndex` (:issue:`57467`)
 - :meth:`Series.str.extract` returns a :class:`RangeIndex` columns instead of an :class:`Index` column when possible (:issue:`57542`)
 - Performance improvement in :class:`DataFrame` when ``data`` is a ``dict`` and ``columns`` is specified (:issue:`24368`)
 - Performance improvement in :meth:`DataFrame.join` for sorted but non-unique indexes (:issue:`56941`)
diff --git a/pandas/_config/config.py b/pandas/_config/config.py
index 8ad1da732a..9decc7eecf 100644
--- a/pandas/_config/config.py
+++ b/pandas/_config/config.py
@@ -50,17 +50,12 @@ Implementation
 
 from __future__ import annotations
 
-from contextlib import (
-    ContextDecorator,
-    contextmanager,
-)
-from inspect import signature
+from contextlib import contextmanager
 import re
 from typing import (
     TYPE_CHECKING,
     Any,
     Callable,
-    Generic,
     Literal,
     NamedTuple,
     cast,
@@ -68,10 +63,7 @@ from typing import (
 )
 import warnings
 
-from pandas._typing import (
-    F,
-    T,
-)
+from pandas._typing import F
 from pandas.util._exceptions import find_stack_level
 
 if TYPE_CHECKING:
@@ -128,68 +120,168 @@ class OptionError(AttributeError, KeyError):
 # User API
 
 
-def _get_single_key(pat: str, silent: bool) -> str:
+def _get_single_key(pat: str) -> str:
     keys = _select_options(pat)
     if len(keys) == 0:
-        if not silent:
-            _warn_if_deprecated(pat)
+        _warn_if_deprecated(pat)
         raise OptionError(f"No such keys(s): {pat!r}")
     if len(keys) > 1:
         raise OptionError("Pattern matched multiple keys")
     key = keys[0]
 
-    if not silent:
-        _warn_if_deprecated(key)
+    _warn_if_deprecated(key)
 
     key = _translate_key(key)
 
     return key
 
 
-def _get_option(pat: str, silent: bool = False) -> Any:
-    key = _get_single_key(pat, silent)
+def get_option(pat: str) -> Any:
+    """
+    Retrieve the value of the specified option.
+
+    Parameters
+    ----------
+    pat : str
+        Regexp which should match a single option.
+
+        .. warning::
+
+            Partial matches are supported for convenience, but unless you use the
+            full option name (e.g. x.y.z.option_name), your code may break in future
+            versions if new options with similar names are introduced.
+
+    Returns
+    -------
+    Any
+        The value of the option.
+
+    Raises
+    ------
+    OptionError : if no such option exists
+
+    Notes
+    -----
+    For all available options, please view the :ref:`User Guide <options.available>`
+    or use ``pandas.describe_option()``.
+
+    Examples
+    --------
+    >>> pd.get_option("display.max_columns")  # doctest: +SKIP
+    4
+    """
+    key = _get_single_key(pat)
 
     # walk the nested dict
     root, k = _get_root(key)
     return root[k]
 
 
-def _set_option(*args, **kwargs) -> None:
+def set_option(*args) -> None:
+    """
+    Set the value of the specified option or options.
+
+    Parameters
+    ----------
+    *args : str | object
+        Arguments provided in pairs, which will be interpreted as (pattern, value)
+        pairs.
+        pattern: str
+        Regexp which should match a single option
+        value: object
+        New value of option
+
+        .. warning::
+
+            Partial pattern matches are supported for convenience, but unless you
+            use the full option name (e.g. x.y.z.option_name), your code may break in
+            future versions if new options with similar names are introduced.
+
+    Returns
+    -------
+    None
+        No return value.
+
+    Raises
+    ------
+    ValueError if odd numbers of non-keyword arguments are provided
+    TypeError if keyword arguments are provided
+    OptionError if no such option exists
+
+    Notes
+    -----
+    For all available options, please view the :ref:`User Guide <options.available>`
+    or use ``pandas.describe_option()``.
+
+    Examples
+    --------
+    >>> pd.set_option("display.max_columns", 4)
+    >>> df = pd.DataFrame([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
+    >>> df
+    0  1  ...  3   4
+    0  1  2  ...  4   5
+    1  6  7  ...  9  10
+    [2 rows x 5 columns]
+    >>> pd.reset_option("display.max_columns")
+    """
     # must at least 1 arg deal with constraints later
     nargs = len(args)
     if not nargs or nargs % 2 != 0:
         raise ValueError("Must provide an even number of non-keyword arguments")
 
-    # default to false
-    silent = kwargs.pop("silent", False)
-
-    if kwargs:
-        kwarg = next(iter(kwargs.keys()))
-        raise TypeError(f'_set_option() got an unexpected keyword argument "{kwarg}"')
-
     for k, v in zip(args[::2], args[1::2]):
-        key = _get_single_key(k, silent)
+        key = _get_single_key(k)
 
-        o = _get_registered_option(key)
-        if o and o.validator:
-            o.validator(v)
+        opt = _get_registered_option(key)
+        if opt and opt.validator:
+            opt.validator(v)
 
         # walk the nested dict
         root, k_root = _get_root(key)
         root[k_root] = v
 
-        if o.cb:
-            if silent:
-                with warnings.catch_warnings(record=True):
-                    o.cb(key)
-            else:
-                o.cb(key)
+        if opt.cb:
+            opt.cb(key)
+
+
+def describe_option(pat: str = "", _print_desc: bool = True) -> str | None:
+    """
+    Print the description for one or more registered options.
+
+    Call with no arguments to get a listing for all registered options.
+
+    Parameters
+    ----------
+    pat : str, default ""
+        String or string regexp pattern.
+        Empty string will return all options.
+        For regexp strings, all matching keys will have their description displayed.
+    _print_desc : bool, default True
+        If True (default) the description(s) will be printed to stdout.
+        Otherwise, the description(s) will be returned as a string
+        (for testing).
+
+    Returns
+    -------
+    None
+        If ``_print_desc=True``.
+    str
+        If the description(s) as a string if ``_print_desc=False``.
 
+    Notes
+    -----
+    For all available options, please view the
+    :ref:`User Guide <options.available>`.
 
-def _describe_option(pat: str = "", _print_desc: bool = True) -> str | None:
+    Examples
+    --------
+    >>> pd.describe_option("display.max_columns")  # doctest: +SKIP
+    display.max_columns : int
+        If max_cols is exceeded, switch to truncate view...
+    """
     keys = _select_options(pat)
     if len(keys) == 0:
-        raise OptionError("No such keys(s)")
+        raise OptionError(f"No such keys(s) for {pat=}")
 
     s = "\n".join([_build_option_description(k) for k in keys])
 
@@ -199,11 +291,40 @@ def _describe_option(pat: str = "", _print_desc: bool = True) -> str | None:
     return s
 
 
-def _reset_option(pat: str, silent: bool = False) -> None:
+def reset_option(pat: str) -> None:
+    """
+    Reset one or more options to their default value.
+
+    Parameters
+    ----------
+    pat : str/regex
+        If specified only options matching ``pat*`` will be reset.
+        Pass ``"all"`` as argument to reset all options.
+
+        .. warning::
+
+            Partial matches are supported for convenience, but unless you
+            use the full option name (e.g. x.y.z.option_name), your code may break
+            in future versions if new options with similar names are introduced.
+
+    Returns
+    -------
+    None
+        No return value.
+
+    Notes
+    -----
+    For all available options, please view the
+    :ref:`User Guide <options.available>`.
+
+    Examples
+    --------
+    >>> pd.reset_option("display.max_columns")  # doctest: +SKIP
+    """
     keys = _select_options(pat)
 
     if len(keys) == 0:
-        raise OptionError("No such keys(s)")
+        raise OptionError(f"No such keys(s) for {pat=}")
 
     if len(keys) > 1 and len(pat) < 4 and pat != "all":
         raise ValueError(
@@ -213,11 +334,11 @@ def _reset_option(pat: str, silent: bool = False) -> None:
         )
 
     for k in keys:
-        _set_option(k, _registered_options[k].defval, silent=silent)
+        set_option(k, _registered_options[k].defval)
 
 
 def get_default_val(pat: str):
-    key = _get_single_key(pat, silent=True)
+    key = _get_single_key(pat)
     return _get_registered_option(key).defval
 
 
@@ -238,7 +359,7 @@ class DictWrapper:
         # you can't set new keys
         # can you can't overwrite subtrees
         if key in self.d and not isinstance(self.d[key], dict):
-            _set_option(prefix, val)
+            set_option(prefix, val)
         else:
             raise OptionError("You can only set the value of existing options")
 
@@ -254,224 +375,38 @@ class DictWrapper:
         if isinstance(v, dict):
             return DictWrapper(v, prefix)
         else:
-            return _get_option(prefix)
+            return get_option(prefix)
 
     def __dir__(self) -> list[str]:
         return list(self.d.keys())
 
 
-# For user convenience,  we'd like to have the available options described
-# in the docstring. For dev convenience we'd like to generate the docstrings
-# dynamically instead of maintaining them by hand. To this, we use the
-# class below which wraps functions inside a callable, and converts
-# __doc__ into a property function. The doctsrings below are templates
-# using the py2.6+ advanced formatting syntax to plug in a concise list
-# of options, and option descriptions.
-
-
-class CallableDynamicDoc(Generic[T]):
-    def __init__(self, func: Callable[..., T], doc_tmpl: str) -> None:
-        self.__doc_tmpl__ = doc_tmpl
-        self.__func__ = func
-        self.__signature__ = signature(func)
-
-    def __call__(self, *args, **kwds) -> T:
-        return self.__func__(*args, **kwds)
-
-    # error: Signature of "__doc__" incompatible with supertype "object"
-    @property
-    def __doc__(self) -> str:  # type: ignore[override]
-        opts_desc = _describe_option("all", _print_desc=False)
-        opts_list = pp_options_list(list(_registered_options.keys()))
-        return self.__doc_tmpl__.format(opts_desc=opts_desc, opts_list=opts_list)
-
-
-_get_option_tmpl = """
-get_option(pat)
-
-Retrieves the value of the specified option.
-
-Available options:
-
-{opts_list}
-
-Parameters
-----------
-pat : str
-    Regexp which should match a single option.
-    Note: partial matches are supported for convenience, but unless you use the
-    full option name (e.g. x.y.z.option_name), your code may break in future
-    versions if new options with similar names are introduced.
-
-Returns
--------
-result : the value of the option
-
-Raises
-------
-OptionError : if no such option exists
-
-Notes
------
-Please reference the :ref:`User Guide <options>` for more information.
-
-The available options with its descriptions:
-
-{opts_desc}
-
-Examples
---------
->>> pd.get_option('display.max_columns')  # doctest: +SKIP
-4
-"""
-
-_set_option_tmpl = """
-set_option(*args, **kwargs)
-
-Sets the value of the specified option or options.
-
-Available options:
-
-{opts_list}
-
-Parameters
-----------
-*args : str | object
-    Arguments provided in pairs, which will be interpreted as (pattern, value)
-    pairs.
-    pattern: str
-    Regexp which should match a single option
-    value: object
-    New value of option
-    Note: partial pattern matches are supported for convenience, but unless you
-    use the full option name (e.g. x.y.z.option_name), your code may break in
-    future versions if new options with similar names are introduced.
-**kwargs : str
-    Keyword arguments are not currently supported.
-
-Returns
--------
-None
-
-Raises
-------
-ValueError if odd numbers of non-keyword arguments are provided
-TypeError if keyword arguments are provided
-OptionError if no such option exists
-
-Notes
------
-Please reference the :ref:`User Guide <options>` for more information.
-
-The available options with its descriptions:
-
-{opts_desc}
-
-Examples
---------
->>> pd.set_option('display.max_columns', 4)
->>> df = pd.DataFrame([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
->>> df
-   0  1  ...  3   4
-0  1  2  ...  4   5
-1  6  7  ...  9  10
-[2 rows x 5 columns]
->>> pd.reset_option('display.max_columns')
-"""
-
-_describe_option_tmpl = """
-describe_option(pat, _print_desc=False)
-
-Prints the description for one or more registered options.
-
-Call with no arguments to get a listing for all registered options.
-
-Available options:
-
-{opts_list}
-
-Parameters
-----------
-pat : str
-    Regexp pattern. All matching keys will have their description displayed.
-_print_desc : bool, default True
-    If True (default) the description(s) will be printed to stdout.
-    Otherwise, the description(s) will be returned as a unicode string
-    (for testing).
-
-Returns
--------
-None by default, the description(s) as a unicode string if _print_desc
-is False
-
-Notes
------
-Please reference the :ref:`User Guide <options>` for more information.
-
-The available options with its descriptions:
-
-{opts_desc}
-
-Examples
---------
->>> pd.describe_option('display.max_columns')  # doctest: +SKIP
-display.max_columns : int
-    If max_cols is exceeded, switch to truncate view...
-"""
-
-_reset_option_tmpl = """
-reset_option(pat)
-
-Reset one or more options to their default value.
-
-Pass "all" as argument to reset all options.
-
-Available options:
-
-{opts_list}
-
-Parameters
-----------
-pat : str/regex
-    If specified only options matching `prefix*` will be reset.
-    Note: partial matches are supported for convenience, but unless you
-    use the full option name (e.g. x.y.z.option_name), your code may break
-    in future versions if new options with similar names are introduced.
-
-Returns
--------
-None
-
-Notes
------
-Please reference the :ref:`User Guide <options>` for more information.
-
-The available options with its descriptions:
-
-{opts_desc}
-
-Examples
---------
->>> pd.reset_option('display.max_columns')  # doctest: +SKIP
-"""
-
-# bind the functions with their docstrings into a Callable
-# and use that as the functions exposed in pd.api
-get_option = CallableDynamicDoc(_get_option, _get_option_tmpl)
-set_option = CallableDynamicDoc(_set_option, _set_option_tmpl)
-reset_option = CallableDynamicDoc(_reset_option, _reset_option_tmpl)
-describe_option = CallableDynamicDoc(_describe_option, _describe_option_tmpl)
 options = DictWrapper(_global_config)
 
 #
 # Functions for use by pandas developers, in addition to User - api
 
 
-class option_context(ContextDecorator):
+@contextmanager
+def option_context(*args) -> Generator[None, None, None]:
     """
-    Context manager to temporarily set options in the `with` statement context.
+    Context manager to temporarily set options in a ``with`` statement.
 
-    You need to invoke as ``option_context(pat, val, [(pat, val), ...])``.
+    Parameters
+    ----------
+    *args : str | object
+        An even amount of arguments provided in pairs which will be
+        interpreted as (pattern, value) pairs.
+
+    Returns
+    -------
+    None
+        No return value.
+
+    Notes
+    -----
+    For all available options, please view the :ref:`User Guide <options.available>`
+    or use ``pandas.describe_option()``.
 
     Examples
     --------
@@ -479,25 +414,21 @@ class option_context(ContextDecorator):
     >>> with option_context("display.max_rows", 10, "display.max_columns", 5):
     ...     pass
     """
+    if len(args) % 2 != 0 or len(args) < 2:
+        raise ValueError(
+            "Provide an even amount of arguments as "
+            "option_context(pat, val, pat, val...)."
+        )
 
-    def __init__(self, *args) -> None:
-        if len(args) % 2 != 0 or len(args) < 2:
-            raise ValueError(
-                "Need to invoke as option_context(pat, val, [(pat, val), ...])."
-            )
-
-        self.ops = list(zip(args[::2], args[1::2]))
-
-    def __enter__(self) -> None:
-        self.undo = [(pat, _get_option(pat)) for pat, val in self.ops]
-
-        for pat, val in self.ops:
-            _set_option(pat, val, silent=True)
-
-    def __exit__(self, *args) -> None:
-        if self.undo:
-            for pat, val in self.undo:
-                _set_option(pat, val, silent=True)
+    ops = tuple(zip(args[::2], args[1::2]))
+    try:
+        undo = tuple((pat, get_option(pat)) for pat, val in ops)
+        for pat, val in ops:
+            set_option(pat, val)
+        yield
+    finally:
+        for pat, val in undo:
+            set_option(pat, val)
 
 
 def register_option(
@@ -740,7 +671,10 @@ def _build_option_description(k: str) -> str:
         s += "No description available."
 
     if o:
-        s += f"\n    [default: {o.defval}] [currently: {_get_option(k, True)}]"
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore", FutureWarning)
+            warnings.simplefilter("ignore", DeprecationWarning)
+            s += f"\n    [default: {o.defval}] [currently: {get_option(k)}]"
 
     if d:
         rkey = d.rkey or ""
diff --git a/pandas/core/arrays/arrow/_arrow_utils.py b/pandas/core/arrays/arrow/_arrow_utils.py
index 01e496945f..cbc9ce0252 100644
--- a/pandas/core/arrays/arrow/_arrow_utils.py
+++ b/pandas/core/arrays/arrow/_arrow_utils.py
@@ -5,7 +5,7 @@ import warnings
 import numpy as np
 import pyarrow
 
-from pandas._config.config import _get_option
+from pandas._config.config import get_option
 
 from pandas.errors import PerformanceWarning
 from pandas.util._exceptions import find_stack_level
@@ -16,7 +16,7 @@ def fallback_performancewarning(version: str | None = None) -> None:
     Raise a PerformanceWarning for falling back to ExtensionArray's
     non-pyarrow method
     """
-    if _get_option("performance_warnings"):
+    if get_option("performance_warnings"):
         msg = "Falling back on a non-pyarrow code path which may decrease performance."
         if version is not None:
             msg += f" Upgrade to pyarrow >={version} to possibly suppress this warning."
diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py
index 3f46c2896a..14967bb811 100644
--- a/pandas/core/arrays/datetimelike.py
+++ b/pandas/core/arrays/datetimelike.py
@@ -20,7 +20,7 @@ import warnings
 
 import numpy as np
 
-from pandas._config.config import _get_option
+from pandas._config.config import get_option
 
 from pandas._libs import (
     algos,
@@ -1336,7 +1336,7 @@ class DatetimeLikeArrayMixin(  # type: ignore[misc]
             # If both 1D then broadcasting is unambiguous
             return op(self, other[0])
 
-        if _get_option("performance_warnings"):
+        if get_option("performance_warnings"):
             warnings.warn(
                 "Adding/subtracting object-dtype array to "
                 f"{type(self).__name__} not vectorized.",
diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py
index 1151669280..4ef5c04461 100644
--- a/pandas/core/arrays/datetimes.py
+++ b/pandas/core/arrays/datetimes.py
@@ -15,7 +15,7 @@ import warnings
 
 import numpy as np
 
-from pandas._config.config import _get_option
+from pandas._config.config import get_option
 
 from pandas._libs import (
     lib,
@@ -820,7 +820,7 @@ class DatetimeArray(dtl.TimelikeOps, dtl.DatelikeOps):  # type: ignore[misc]
                 # "dtype[Any] | type[Any] | _SupportsDType[dtype[Any]]"
                 res_values = res_values.view(values.dtype)  # type: ignore[arg-type]
         except NotImplementedError:
-            if _get_option("performance_warnings"):
+            if get_option("performance_warnings"):
                 warnings.warn(
                     "Non-vectorized DateOffset being applied to Series or "
                     "DatetimeIndex.",
diff --git a/pandas/core/arrays/sparse/array.py b/pandas/core/arrays/sparse/array.py
index 48147f10ba..9b1d4d70ee 100644
--- a/pandas/core/arrays/sparse/array.py
+++ b/pandas/core/arrays/sparse/array.py
@@ -18,7 +18,7 @@ import warnings
 
 import numpy as np
 
-from pandas._config.config import _get_option
+from pandas._config.config import get_option
 
 from pandas._libs import lib
 import pandas._libs.sparse as splib
@@ -1158,7 +1158,7 @@ class SparseArray(OpsMixin, PandasObject, ExtensionArray):
         side: Literal["left", "right"] = "left",
         sorter: NumpySorter | None = None,
     ) -> npt.NDArray[np.intp] | np.intp:
-        if _get_option("performance_warnings"):
+        if get_option("performance_warnings"):
             msg = "searchsorted requires high memory usage."
             warnings.warn(msg, PerformanceWarning, stacklevel=find_stack_level())
         v = np.asarray(v)
diff --git a/pandas/core/arrays/string_arrow.py b/pandas/core/arrays/string_arrow.py
index 195efa3576..ec2534ce17 100644
--- a/pandas/core/arrays/string_arrow.py
+++ b/pandas/core/arrays/string_arrow.py
@@ -12,7 +12,7 @@ from typing import (
 
 import numpy as np
 
-from pandas._config.config import _get_option
+from pandas._config.config import get_option
 
 from pandas._libs import (
     lib,
@@ -345,7 +345,7 @@ class ArrowStringArray(ObjectStringArrayMixin, ArrowExtensionArray, BaseStringAr
         self, pat, case: bool = True, flags: int = 0, na=np.nan, regex: bool = True
     ):
         if flags:
-            if _get_option("mode.performance_warnings"):
+            if get_option("mode.performance_warnings"):
                 fallback_performancewarning()
             return super()._str_contains(pat, case, flags, na, regex)
 
@@ -406,7 +406,7 @@ class ArrowStringArray(ObjectStringArrayMixin, ArrowExtensionArray, BaseStringAr
         regex: bool = True,
     ):
         if isinstance(pat, re.Pattern) or callable(repl) or not case or flags:
-            if _get_option("mode.performance_warnings"):
+            if get_option("mode.performance_warnings"):
                 fallback_performancewarning()
             return super()._str_replace(pat, repl, n, case, flags, regex)
 
diff --git a/pandas/core/computation/align.py b/pandas/core/computation/align.py
index 18329e8230..2a48bb280a 100644
--- a/pandas/core/computation/align.py
+++ b/pandas/core/computation/align.py
@@ -15,7 +15,7 @@ import warnings
 
 import numpy as np
 
-from pandas._config.config import _get_option
+from pandas._config.config import get_option
 
 from pandas.errors import PerformanceWarning
 from pandas.util._exceptions import find_stack_level
@@ -127,7 +127,7 @@ def _align_core(terms):
 
                 ordm = np.log10(max(1, abs(reindexer_size - term_axis_size)))
                 if (
-                    _get_option("performance_warnings")
+                    get_option("performance_warnings")
                     and ordm >= 1
                     and reindexer_size >= 10000
                 ):
diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py
index 02560f54e2..27b9c0dec2 100644
--- a/pandas/core/dtypes/dtypes.py
+++ b/pandas/core/dtypes/dtypes.py
@@ -21,7 +21,7 @@ import warnings
 import numpy as np
 import pytz
 
-from pandas._config.config import _get_option
+from pandas._config.config import get_option
 
 from pandas._libs import (
     lib,
@@ -2030,7 +2030,7 @@ class SparseDtype(ExtensionDtype):
 
         # np.nan isn't a singleton, so we may end up with multiple
         # NaNs here, so we ignore the all NA case too.
-        if _get_option("performance_warnings") and (
+        if get_option("performance_warnings") and (
             not (len(set(fill_values)) == 1 or isna(fill_values).all())
         ):
             warnings.warn(
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index 928771f9d7..3ab40c1aeb 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -6506,15 +6506,15 @@ class DataFrame(NDFrame, OpsMixin):
         DataFrame or None
             DataFrame with duplicates removed or None if ``inplace=True``.
 
-        Notes
-        -------
-        This method requires columns specified by ``subset`` to be of hashable type.
-        Passing unhashable columns will raise a ``TypeError``.
-
         See Also
         --------
         DataFrame.value_counts: Count unique combinations of columns.
 
+        Notes
+        -----
+        This method requires columns specified by ``subset`` to be of hashable type.
+        Passing unhashable columns will raise a ``TypeError``.
+
         Examples
         --------
         Consider dataset containing ramen rating.
diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py
index 9449e6d7ab..52fd7735b5 100644
--- a/pandas/core/groupby/generic.py
+++ b/pandas/core/groupby/generic.py
@@ -1179,8 +1179,7 @@ class SeriesGroupBy(GroupBy[Series]):
         Parameters
         ----------
         skipna : bool, default True
-            Exclude NA/null values. If the entire Series is NA, the result
-            will be NA.
+            Exclude NA values.
 
         Returns
         -------
@@ -1190,7 +1189,7 @@ class SeriesGroupBy(GroupBy[Series]):
         Raises
         ------
         ValueError
-            If the Series is empty.
+            If the Series is empty or skipna=False and any value is NA.
 
         See Also
         --------
@@ -1233,8 +1232,7 @@ class SeriesGroupBy(GroupBy[Series]):
         Parameters
         ----------
         skipna : bool, default True
-            Exclude NA/null values. If the entire Series is NA, the result
-            will be NA.
+            Exclude NA values.
 
         Returns
         -------
@@ -1244,7 +1242,7 @@ class SeriesGroupBy(GroupBy[Series]):
         Raises
         ------
         ValueError
-            If the Series is empty.
+            If the Series is empty or skipna=False and any value is NA.
 
         See Also
         --------
@@ -2165,13 +2163,10 @@ class DataFrameGroupBy(GroupBy[DataFrame]):
         """
         Return index of first occurrence of maximum in each group.
 
-        NA/null values are excluded.
-
         Parameters
         ----------
         skipna : bool, default True
-            Exclude NA/null values. If an entire row/column is NA, the result
-            will be NA.
+            Exclude NA values.
         numeric_only : bool, default False
             Include only `float`, `int` or `boolean` data.
 
@@ -2185,7 +2180,7 @@ class DataFrameGroupBy(GroupBy[DataFrame]):
         Raises
         ------
         ValueError
-            * If the row/column is empty
+            * If a column is empty or skipna=False and any value is NA.
 
         See Also
         --------
@@ -2230,13 +2225,10 @@ class DataFrameGroupBy(GroupBy[DataFrame]):
         """
         Return index of first occurrence of minimum in each group.
 
-        NA/null values are excluded.
-
         Parameters
         ----------
         skipna : bool, default True
-            Exclude NA/null values. If an entire row/column is NA, the result
-            will be NA.
+            Exclude NA values.
         numeric_only : bool, default False
             Include only `float`, `int` or `boolean` data.
 
@@ -2250,7 +2242,7 @@ class DataFrameGroupBy(GroupBy[DataFrame]):
         Raises
         ------
         ValueError
-            * If the row/column is empty
+            * If a column is empty or skipna=False and any value is NA.
 
         See Also
         --------
diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py
index 61168f71f4..d90ef41058 100644
--- a/pandas/core/groupby/groupby.py
+++ b/pandas/core/groupby/groupby.py
@@ -5553,15 +5553,11 @@ class GroupBy(BaseGroupBy[NDFrameT]):
                     f"Can't get {how} of an empty group due to unobserved categories. "
                     "Specify observed=True in groupby instead."
                 )
-        elif not skipna:
-            if self._obj_with_exclusions.isna().any(axis=None):
-                warnings.warn(
-                    f"The behavior of {type(self).__name__}.{how} with all-NA "
-                    "values, or any-NA and skipna=False, is deprecated. In a future "
-                    "version this will raise ValueError",
-                    FutureWarning,
-                    stacklevel=find_stack_level(),
-                )
+        elif not skipna and self._obj_with_exclusions.isna().any(axis=None):
+            raise ValueError(
+                f"{type(self).__name__}.{how} with skipna=False encountered an NA "
+                f"value."
+            )
 
         result = self._agg_general(
             numeric_only=numeric_only,
diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py
index a1ca9727c1..bfebf126ec 100644
--- a/pandas/core/indexes/multi.py
+++ b/pandas/core/indexes/multi.py
@@ -21,7 +21,6 @@ import warnings
 import numpy as np
 
 from pandas._config import get_option
-from pandas._config.config import _get_option
 
 from pandas._libs import (
     algos as libalgos,
@@ -2380,7 +2379,7 @@ class MultiIndex(Index):
                     step = loc.step if loc.step is not None else 1
                     inds.extend(range(loc.start, loc.stop, step))
                 elif com.is_bool_indexer(loc):
-                    if _get_option("performance_warnings") and self._lexsort_depth == 0:
+                    if get_option("performance_warnings") and self._lexsort_depth == 0:
                         warnings.warn(
                             "dropping on a non-lexsorted multi-index "
                             "without a level parameter may impact performance.",
@@ -3042,7 +3041,7 @@ class MultiIndex(Index):
         if not follow_key:
             return slice(start, stop)
 
-        if _get_option("performance_warnings"):
+        if get_option("performance_warnings"):
             warnings.warn(
                 "indexing past lexsort depth may impact performance.",
                 PerformanceWarning,
diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py
index 09d635b53c..0781a86e5d 100644
--- a/pandas/core/indexes/range.py
+++ b/pandas/core/indexes/range.py
@@ -989,7 +989,10 @@ class RangeIndex(Index):
         indexes = [RangeIndex(3), RangeIndex(4, 6)] -> Index([0,1,2,4,5], dtype='int64')
         """
         if not all(isinstance(x, RangeIndex) for x in indexes):
-            return super()._concat(indexes, name)
+            result = super()._concat(indexes, name)
+            if result.dtype.kind == "i":
+                return self._shallow_copy(result._values)
+            return result
 
         elif len(indexes) == 1:
             return indexes[0]
diff --git a/pandas/core/internals/managers.py b/pandas/core/internals/managers.py
index f1cbfb39b0..46716bb8bf 100644
--- a/pandas/core/internals/managers.py
+++ b/pandas/core/internals/managers.py
@@ -18,7 +18,7 @@ import warnings
 
 import numpy as np
 
-from pandas._config.config import _get_option
+from pandas._config.config import get_option
 
 from pandas._libs import (
     algos as libalgos,
@@ -1529,7 +1529,7 @@ class BlockManager(libinternals.BlockManager, BaseBlockManager):
         self._known_consolidated = False
 
         if (
-            _get_option("performance_warnings")
+            get_option("performance_warnings")
             and sum(not block.is_extension for block in self.blocks) > 100
         ):
             warnings.warn(
diff --git a/pandas/core/reshape/reshape.py b/pandas/core/reshape/reshape.py
index 176b00b079..c770acb638 100644
--- a/pandas/core/reshape/reshape.py
+++ b/pandas/core/reshape/reshape.py
@@ -10,7 +10,7 @@ import warnings
 
 import numpy as np
 
-from pandas._config.config import _get_option
+from pandas._config.config import get_option
 
 import pandas._libs.reshape as libreshape
 from pandas.errors import PerformanceWarning
@@ -146,7 +146,7 @@ class _Unstacker:
         num_cells = num_rows * num_columns
 
         # GH 26314: Previous ValueError raised was too restrictive for many users.
-        if _get_option("performance_warnings") and num_cells > np.iinfo(np.int32).max:
+        if get_option("performance_warnings") and num_cells > np.iinfo(np.int32).max:
             warnings.warn(
                 f"The following operation may generate {num_cells} cells "
                 f"in the resulting pandas object.",
diff --git a/pandas/core/shared_docs.py b/pandas/core/shared_docs.py
index 787a03471c..06621f7127 100644
--- a/pandas/core/shared_docs.py
+++ b/pandas/core/shared_docs.py
@@ -178,6 +178,12 @@ See the `user guide
 <https://pandas.pydata.org/pandas-docs/stable/groupby.html>`__ for more
 detailed usage and examples, including splitting an object into groups,
 iterating through groups, selecting a group, aggregation, and more.
+
+The implementation of groupby is hash-based, meaning in particular that
+objects that compare as equal will be considered to be in the same group.
+An exception to this is that pandas has special handling of NA values:
+any NA values will be collapsed to a single group, regardless of how
+they compare. See the user guide linked above for more details.
 """
 
 _shared_docs["melt"] = """
diff --git a/pandas/io/excel/_base.py b/pandas/io/excel/_base.py
index c38ced5735..d77a955e41 100644
--- a/pandas/io/excel/_base.py
+++ b/pandas/io/excel/_base.py
@@ -1131,7 +1131,7 @@ class ExcelWriter(Generic[_WorkbookT]):
                     ext = "xlsx"
 
                 try:
-                    engine = config.get_option(f"io.excel.{ext}.writer", silent=True)
+                    engine = config.get_option(f"io.excel.{ext}.writer")
                     if engine == "auto":
                         engine = get_default_engine(ext, mode="writer")
                 except KeyError as err:
@@ -1552,7 +1552,7 @@ class ExcelFile:
                         "an engine manually."
                     )
 
-            engine = config.get_option(f"io.excel.{ext}.reader", silent=True)
+            engine = config.get_option(f"io.excel.{ext}.reader")
             if engine == "auto":
                 engine = get_default_engine(ext, mode="reader")
 
diff --git a/pandas/io/formats/info.py b/pandas/io/formats/info.py
index a837eddd6c..ad595a2be8 100644
--- a/pandas/io/formats/info.py
+++ b/pandas/io/formats/info.py
@@ -622,7 +622,7 @@ class _DataFrameInfoPrinter(_InfoPrinterAbstract):
     @property
     def max_rows(self) -> int:
         """Maximum info rows to be displayed."""
-        return get_option("display.max_info_rows", len(self.data) + 1)
+        return get_option("display.max_info_rows")
 
     @property
     def exceeds_info_cols(self) -> bool:
@@ -641,7 +641,7 @@ class _DataFrameInfoPrinter(_InfoPrinterAbstract):
 
     def _initialize_max_cols(self, max_cols: int | None) -> int:
         if max_cols is None:
-            return get_option("display.max_info_columns", self.col_count + 1)
+            return get_option("display.max_info_columns")
         return max_cols
 
     def _initialize_show_counts(self, show_counts: bool | None) -> bool:
diff --git a/pandas/io/pytables.py b/pandas/io/pytables.py
index 60ef953059..5703f626e3 100644
--- a/pandas/io/pytables.py
+++ b/pandas/io/pytables.py
@@ -31,7 +31,6 @@ from pandas._config import (
     get_option,
     using_pyarrow_string_dtype,
 )
-from pandas._config.config import _get_option
 
 from pandas._libs import (
     lib,
@@ -3149,7 +3148,7 @@ class GenericFixed(Fixed):
                 pass
             elif inferred_type == "string":
                 pass
-            elif _get_option("performance_warnings"):
+            elif get_option("performance_warnings"):
                 ws = performance_doc % (inferred_type, key, items)
                 warnings.warn(ws, PerformanceWarning, stacklevel=find_stack_level())
 


Test patch:
diff --git a/pandas/tests/config/test_config.py b/pandas/tests/config/test_config.py
index f49ae94242..205603b576 100644
--- a/pandas/tests/config/test_config.py
+++ b/pandas/tests/config/test_config.py
@@ -395,7 +395,7 @@ class TestConfig:
         assert cf.get_option("a") == 500
 
         cf.reset_option("a")
-        assert options.a == cf.get_option("a", 0)
+        assert options.a == cf.get_option("a")
 
         msg = "You can only set the value of existing options"
         with pytest.raises(OptionError, match=msg):
diff --git a/pandas/tests/groupby/test_reductions.py b/pandas/tests/groupby/test_reductions.py
index 2037ded9f2..edc94b2bee 100644
--- a/pandas/tests/groupby/test_reductions.py
+++ b/pandas/tests/groupby/test_reductions.py
@@ -291,16 +291,14 @@ def test_idxmin_idxmax_extremes_skipna(skipna, how, float_numpy_dtype):
     )
     gb = df.groupby("a")
 
-    warn = None if skipna else FutureWarning
-    msg = f"The behavior of DataFrameGroupBy.{how} with all-NA values"
-    with tm.assert_produces_warning(warn, match=msg):
-        result = getattr(gb, how)(skipna=skipna)
-    if skipna:
-        values = [1, 3, 4, 6, np.nan]
-    else:
-        values = np.nan
+    if not skipna:
+        msg = f"DataFrameGroupBy.{how} with skipna=False"
+        with pytest.raises(ValueError, match=msg):
+            getattr(gb, how)(skipna=skipna)
+        return
+    result = getattr(gb, how)(skipna=skipna)
     expected = DataFrame(
-        {"b": values}, index=pd.Index(range(1, 6), name="a", dtype="intp")
+        {"b": [1, 3, 4, 6, np.nan]}, index=pd.Index(range(1, 6), name="a", dtype="intp")
     )
     tm.assert_frame_equal(result, expected)
 
diff --git a/pandas/tests/groupby/transform/test_transform.py b/pandas/tests/groupby/transform/test_transform.py
index db327cc689..0b4dfb41ab 100644
--- a/pandas/tests/groupby/transform/test_transform.py
+++ b/pandas/tests/groupby/transform/test_transform.py
@@ -1525,10 +1525,11 @@ def test_idxmin_idxmax_transform_args(how, skipna, numeric_only):
     # GH#55268 - ensure *args are passed through when calling transform
     df = DataFrame({"a": [1, 1, 1, 2], "b": [3.0, 4.0, np.nan, 6.0], "c": list("abcd")})
     gb = df.groupby("a")
-    warn = None if skipna else FutureWarning
-    msg = f"The behavior of DataFrameGroupBy.{how} with .* any-NA and skipna=False"
-    with tm.assert_produces_warning(warn, match=msg):
+    if skipna:
         result = gb.transform(how, skipna, numeric_only)
-    with tm.assert_produces_warning(warn, match=msg):
         expected = gb.transform(how, skipna=skipna, numeric_only=numeric_only)
-    tm.assert_frame_equal(result, expected)
+        tm.assert_frame_equal(result, expected)
+    else:
+        msg = f"DataFrameGroupBy.{how} with skipna=False encountered an NA value"
+        with pytest.raises(ValueError, match=msg):
+            gb.transform(how, skipna, numeric_only)
diff --git a/pandas/tests/indexes/ranges/test_range.py b/pandas/tests/indexes/ranges/test_range.py
index 898548d1cc..8c24ce5d69 100644
--- a/pandas/tests/indexes/ranges/test_range.py
+++ b/pandas/tests/indexes/ranges/test_range.py
@@ -608,6 +608,20 @@ class TestRangeIndex:
         tm.assert_index_equal(result, expected)
 
 
+def test_append_non_rangeindex_return_rangeindex():
+    ri = RangeIndex(1)
+    result = ri.append(Index([1]))
+    expected = RangeIndex(2)
+    tm.assert_index_equal(result, expected, exact=True)
+
+
+def test_append_non_rangeindex_return_index():
+    ri = RangeIndex(1)
+    result = ri.append(Index([1, 3, 4]))
+    expected = Index([0, 1, 3, 4])
+    tm.assert_index_equal(result, expected, exact=True)
+
+
 def test_reindex_returns_rangeindex():
     ri = RangeIndex(2, name="foo")
     result, result_indexer = ri.reindex([1, 2, 3])
diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py
index f748d7c5fc..d4774a5cd0 100644
--- a/pandas/tests/plotting/test_converter.py
+++ b/pandas/tests/plotting/test_converter.py
@@ -114,18 +114,17 @@ class TestRegistration:
 
     def test_option_no_warning(self):
         pytest.importorskip("matplotlib.pyplot")
-        ctx = cf.option_context("plotting.matplotlib.register_converters", False)
         plt = pytest.importorskip("matplotlib.pyplot")
         s = Series(range(12), index=date_range("2017", periods=12))
         _, ax = plt.subplots()
 
         # Test without registering first, no warning
-        with ctx:
+        with cf.option_context("plotting.matplotlib.register_converters", False):
             ax.plot(s.index, s.values)
 
         # Now test with registering
         register_matplotlib_converters()
-        with ctx:
+        with cf.option_context("plotting.matplotlib.register_converters", False):
             ax.plot(s.index, s.values)
         plt.close()