From e10220608f810b7dbf859c275769f99428bb93a6 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Mon, 17 May 2021 19:58:45 +0100 Subject: [PATCH 1/2] fixup deprecate_nonkeyword_args --- pandas/io/excel/_base.py | 2 +- .../test_deprecate_nonkeyword_arguments.py | 19 ++++++++- pandas/util/_decorators.py | 41 +++++++++---------- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/pandas/io/excel/_base.py b/pandas/io/excel/_base.py index 4b81b69976c62..cf2246f917bbe 100644 --- a/pandas/io/excel/_base.py +++ b/pandas/io/excel/_base.py @@ -329,7 +329,7 @@ ) -@deprecate_nonkeyword_arguments(allowed_args=2, version="2.0") +@deprecate_nonkeyword_arguments(allowed_args=["io", "sheet_name"], version="2.0") @Appender(_read_excel_doc) def read_excel( io, diff --git a/pandas/tests/util/test_deprecate_nonkeyword_arguments.py b/pandas/tests/util/test_deprecate_nonkeyword_arguments.py index 05bc617232bdd..a20264ac6fbb0 100644 --- a/pandas/tests/util/test_deprecate_nonkeyword_arguments.py +++ b/pandas/tests/util/test_deprecate_nonkeyword_arguments.py @@ -68,7 +68,7 @@ def test_three_positional_argument_with_warning_message_analysis(): for actual_warning in w: assert actual_warning.category == FutureWarning assert str(actual_warning.message) == ( - "Starting with Pandas version 1.1 all arguments of g " + "Starting with pandas version 1.1 all arguments of g " "except for the argument 'a' will be keyword-only" ) @@ -96,6 +96,21 @@ def test_one_positional_argument_with_warning_message_analysis(): for actual_warning in w: assert actual_warning.category == FutureWarning assert str(actual_warning.message) == ( - "Starting with Pandas version 1.1 all arguments " + "Starting with pandas version 1.1 all arguments " "of h will be keyword-only" ) + + +class Foo: + @deprecate_nonkeyword_arguments(version=None, allowed_args=["self", "bar"]) + def baz(self, bar=None, foobar=None): + ... + + +def test_class(): + msg = ( + r"In a future version of pandas all arguments of Foo\.baz " + r"except for the argument \'bar\' will be keyword-only" + ) + with tm.assert_produces_warning(FutureWarning, match=msg): + Foo().baz("qux", "quox") diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index 835001b3e1829..42fa97e2d7562 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -211,7 +211,7 @@ def wrapper(*args, **kwargs) -> Callable[..., Any]: return _deprecate_kwarg -def _format_argument_list(allow_args: list[str] | int): +def _format_argument_list(allow_args: list[str]): """ Convert the allow_args argument (either string or integer) of `deprecate_nonkeyword_arguments` function to a string describing @@ -231,21 +231,16 @@ def _format_argument_list(allow_args: list[str] | int): Examples -------- - `format_argument_list(0)` -> '' - `format_argument_list(1)` -> 'except for the first argument' - `format_argument_list(2)` -> 'except for the first 2 arguments' `format_argument_list([])` -> '' `format_argument_list(['a'])` -> "except for the arguments 'a'" `format_argument_list(['a', 'b'])` -> "except for the arguments 'a' and 'b'" `format_argument_list(['a', 'b', 'c'])` -> "except for the arguments 'a', 'b' and 'c'" """ + if "self" in allow_args: + allow_args.remove("self") if not allow_args: return "" - elif allow_args == 1: - return " except for the first argument" - elif isinstance(allow_args, int): - return f" except for the first {allow_args} arguments" elif len(allow_args) == 1: return f" except for the argument '{allow_args[0]}'" else: @@ -254,9 +249,17 @@ def _format_argument_list(allow_args: list[str] | int): return f" except for the arguments {args} and '{last}'" +def future_version_msg(version: str | None) -> str: + """Specify which version of pandas the deprecation will take place in.""" + if version is None: + return "In a future version of pandas" + else: + return f"Starting with pandas version {version}" + + def deprecate_nonkeyword_arguments( - version: str, - allowed_args: list[str] | int | None = None, + version: str | None, + allowed_args: list[str] | None = None, stacklevel: int = 2, ) -> Callable: """ @@ -266,14 +269,13 @@ def deprecate_nonkeyword_arguments( ---------- version : str The version in which positional arguments will become - keyword-only. + keyword-only. If None, then the warning message won't + specify any particular version. - allowed_args : list or int, optional + allowed_args : list, optional In case of list, it must be the list of names of some first arguments of the decorated functions that are - OK to be given as positional arguments. In case of an - integer, this is the number of positional arguments - that will stay positional. In case of None value, + OK to be given as positional arguments. In case of None value, defaults to list of all arguments not having the default value. @@ -294,14 +296,11 @@ def decorate(func): @wraps(func) def wrapper(*args, **kwargs): arguments = _format_argument_list(allow_args) - if isinstance(allow_args, (list, tuple)): - num_allow_args = len(allow_args) - else: - num_allow_args = allow_args + num_allow_args = len(allow_args) if len(args) > num_allow_args: msg = ( - f"Starting with Pandas version {version} all arguments of " - f"{func.__name__}{arguments} will be keyword-only" + f"{future_version_msg(version)} all arguments of " + f"{func.__qualname__}{arguments} will be keyword-only" ) warnings.warn(msg, FutureWarning, stacklevel=stacklevel) return func(*args, **kwargs) From 8a5ce4ba01636abd339f86d641578dc8981d8d80 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Mon, 17 May 2021 21:13:47 +0100 Subject: [PATCH 2/2] move to outer scope --- pandas/util/_decorators.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index 42fa97e2d7562..f4c360f476514 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -293,16 +293,21 @@ def decorate(func): assert spec.defaults is not None # for mypy allow_args = spec.args[: -len(spec.defaults)] + num_allow_args = len(allow_args) + msg = ( + f"{future_version_msg(version)} all arguments of " + f"{func.__qualname__}{{arguments}} will be keyword-only" + ) + @wraps(func) def wrapper(*args, **kwargs): arguments = _format_argument_list(allow_args) - num_allow_args = len(allow_args) if len(args) > num_allow_args: - msg = ( - f"{future_version_msg(version)} all arguments of " - f"{func.__qualname__}{arguments} will be keyword-only" + warnings.warn( + msg.format(arguments=arguments), + FutureWarning, + stacklevel=stacklevel, ) - warnings.warn(msg, FutureWarning, stacklevel=stacklevel) return func(*args, **kwargs) return wrapper