Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ repos:
language: python
entry: python scripts/validate_unwanted_patterns.py --validation-type="nodefault_used_not_only_for_typing"
types: [python]
- id: unwanted-patterns-doesnt-use-pandas-warnings
name: Check that warning classes for deprecations use pandas' warning classes
language: python
entry: python scripts/validate_unwanted_patterns.py --validation-type="doesnt_use_pandas_warnings"
types: [ python ]
- id: no-return-exception
name: Use raise instead of return for exceptions
language: pygrep
Expand Down
8 changes: 6 additions & 2 deletions pandas/_config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ def _warn_if_deprecated(key: str) -> bool:
if d.msg:
warnings.warn(
d.msg,
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
else:
Expand All @@ -728,7 +728,11 @@ def _warn_if_deprecated(key: str) -> bool:
else:
msg += ", please refrain from using it."

warnings.warn(msg, FutureWarning, stacklevel=find_stack_level())
warnings.warn(
msg,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
return True
return False

Expand Down
2 changes: 1 addition & 1 deletion pandas/core/arrays/string_.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def __init__(
"'pd.options.future.infer_string = True' option globally and use "
'the "str" alias as a shorthand notation to specify a dtype '
'(instead of "string[pyarrow_numpy]").',
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
storage = "pyarrow"
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/arrays/string_arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def _convert_bool_result(self, values, na=lib.no_default, method_name=None):
warnings.warn(
f"Allowing a non-bool 'na' in obj.str.{method_name} is deprecated "
"and will raise in a future version.",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
na = bool(na)
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/dtypes/dtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,7 @@ def __new__(cls, freq) -> PeriodDtype: # noqa: PYI034
warnings.warn(
"PeriodDtype[B] is deprecated and will be removed in a future "
"version. Use a DatetimeIndex with freq='B' instead",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)

Expand Down
4 changes: 2 additions & 2 deletions pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -13262,7 +13262,7 @@ def idxmin(
f"The behavior of {type(self).__name__}.idxmin with all-NA "
"values, or any-NA and skipna=False, is deprecated. In a future "
"version this will raise ValueError",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)

Expand Down Expand Up @@ -13369,7 +13369,7 @@ def idxmax(
f"The behavior of {type(self).__name__}.idxmax with all-NA "
"values, or any-NA and skipna=False, is deprecated. In a future "
"version this will raise ValueError",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)

Expand Down
2 changes: 1 addition & 1 deletion pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -9139,7 +9139,7 @@ def resample(
"deprecated and will be removed in a future version. "
"Explicitly cast PeriodIndex to DatetimeIndex before resampling "
"instead.",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
else:
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/groupby/groupby.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ def groups(self) -> dict[Hashable, Index]:
"and will be removed. In a future version `groups` by one element "
"list will return tuple. Use ``df.groupby(by='a').groups`` "
"instead of ``df.groupby(by=['a']).groups`` to avoid this warning",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
return self._grouper.groups
Expand Down
4 changes: 2 additions & 2 deletions pandas/core/resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -1949,7 +1949,7 @@ def _resampler_for_grouping(self):
warnings.warn(
"Resampling a groupby with a PeriodIndex is deprecated. "
"Cast to DatetimeIndex before resampling instead.",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
return PeriodIndexResamplerGroupby
Expand Down Expand Up @@ -2297,7 +2297,7 @@ def _get_resampler(self, obj: NDFrame) -> Resampler:
warnings.warn(
"Resampling with a PeriodIndex is deprecated. "
"Cast index to DatetimeIndex before resampling instead.",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
return PeriodIndexResampler(
Expand Down
6 changes: 3 additions & 3 deletions pandas/core/strings/object_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def _str_contains(
warnings.warn(
"Allowing a non-bool 'na' in obj.str.contains is deprecated "
"and will raise in a future version.",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
return self._str_map(f, na, dtype=np.dtype("bool"))
Expand All @@ -175,7 +175,7 @@ def _str_startswith(self, pat, na=lib.no_default):
warnings.warn(
"Allowing a non-bool 'na' in obj.str.startswith is deprecated "
"and will raise in a future version.",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
return self._str_map(f, na_value=na, dtype=np.dtype(bool))
Expand All @@ -187,7 +187,7 @@ def _str_endswith(self, pat, na=lib.no_default):
warnings.warn(
"Allowing a non-bool 'na' in obj.str.endswith is deprecated "
"and will raise in a future version.",
FutureWarning,
FutureWarning, # pdlint: ignore[warning_class]
stacklevel=find_stack_level(),
)
return self._str_map(f, na_value=na, dtype=np.dtype(bool))
Expand Down
24 changes: 15 additions & 9 deletions pandas/tests/util/test_assert_produces_warning.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def pair_different_warnings(request):


def f():
warnings.warn("f1", FutureWarning)
warnings.warn("f1", FutureWarning) # pdlint: ignore[warning_class]
warnings.warn("f2", RuntimeWarning)


Expand Down Expand Up @@ -175,7 +175,7 @@ def test_match_multiple_warnings():
# https://github.com/pandas-dev/pandas/issues/47829
category = (FutureWarning, UserWarning)
with tm.assert_produces_warning(category, match=r"^Match this"):
warnings.warn("Match this", FutureWarning)
warnings.warn("Match this", FutureWarning) # pdlint: ignore[warning_class]
warnings.warn("Match this too", UserWarning)


Expand All @@ -185,7 +185,7 @@ def test_must_match_multiple_warnings():
msg = "Did not see expected warning of class 'UserWarning'"
with pytest.raises(AssertionError, match=msg):
with tm.assert_produces_warning(category, match=r"^Match this"):
warnings.warn("Match this", FutureWarning)
warnings.warn("Match this", FutureWarning) # pdlint: ignore[warning_class]


def test_must_match_multiple_warnings_messages():
Expand All @@ -194,7 +194,7 @@ def test_must_match_multiple_warnings_messages():
msg = r"The emitted warning messages are \[UserWarning\('Not this'\)\]"
with pytest.raises(AssertionError, match=msg):
with tm.assert_produces_warning(category, match=r"^Match this"):
warnings.warn("Match this", FutureWarning)
warnings.warn("Match this", FutureWarning) # pdlint: ignore[warning_class]
warnings.warn("Not this", UserWarning)


Expand All @@ -204,7 +204,7 @@ def test_allow_partial_match_for_multiple_warnings():
with tm.assert_produces_warning(
category, match=r"^Match this", must_find_all_warnings=False
):
warnings.warn("Match this", FutureWarning)
warnings.warn("Match this", FutureWarning) # pdlint: ignore[warning_class]


def test_allow_partial_match_for_multiple_warnings_messages():
Expand All @@ -213,7 +213,7 @@ def test_allow_partial_match_for_multiple_warnings_messages():
with tm.assert_produces_warning(
category, match=r"^Match this", must_find_all_warnings=False
):
warnings.warn("Match this", FutureWarning)
warnings.warn("Match this", FutureWarning) # pdlint: ignore[warning_class]
warnings.warn("Not this", UserWarning)


Expand Down Expand Up @@ -250,13 +250,17 @@ def test_raises_during_exception():

with pytest.raises(AssertionError, match=msg):
with tm.assert_produces_warning(UserWarning):
warnings.warn("FutureWarning", FutureWarning)
warnings.warn(
"FutureWarning", FutureWarning
) # pdlint: ignore[warning_class]
raise IndexError

msg = "Caused unexpected warning"
with pytest.raises(AssertionError, match=msg):
with tm.assert_produces_warning(None):
warnings.warn("FutureWarning", FutureWarning)
warnings.warn(
"FutureWarning", FutureWarning
) # pdlint: ignore[warning_class]
raise SystemError


Expand All @@ -267,5 +271,7 @@ def test_passes_during_exception():

with pytest.raises(ValueError, match="Error"):
with tm.assert_produces_warning(FutureWarning, match="FutureWarning"):
warnings.warn("FutureWarning", FutureWarning)
warnings.warn(
"FutureWarning", FutureWarning
) # pdlint: ignore[warning_class]
raise ValueError("Error")
5 changes: 4 additions & 1 deletion pandas/tests/util/test_rewrite_warning.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ def test_rewrite_warning(target_category, target_message, hit, new_category):
with rewrite_warning(
target_message, target_category, new_message, new_category
):
warnings.warn(message="Target message", category=FutureWarning)
warnings.warn(
message="Target message",
category=FutureWarning, # pdlint: ignore[warning_class]
)
34 changes: 34 additions & 0 deletions scripts/tests/test_validate_unwanted_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,37 @@ def test_nodefault_used_not_only_for_typing_raises(self, data, expected) -> None
fd = io.StringIO(data.strip())
result = list(validate_unwanted_patterns.nodefault_used_not_only_for_typing(fd))
assert result == expected


@pytest.mark.parametrize("function", ["warnings.warn", "warn"])
@pytest.mark.parametrize("positional", [True, False])
@pytest.mark.parametrize(
"category",
[
"FutureWarning",
"DeprecationWarning",
"PendingDeprecationWarning",
"Pandas4Warning",
"RuntimeWarning"
],
)
@pytest.mark.parametrize("pdlint_ignore", [True, False])
def test_doesnt_use_pandas_warnings(function, positional, category, pdlint_ignore):
code = (
f"{function}({' # pdlint: ignore[warning_class]' if pdlint_ignore else ''}\n"
f' "message",\n'
f" {'' if positional else 'category='}{category},\n"
f")\n"
)
flag_issue = (
category in ["FutureWarning", "DeprecationWarning", "PendingDeprecationWarning"]
and not pdlint_ignore
)
fd = io.StringIO(code)
result = list(validate_unwanted_patterns.doesnt_use_pandas_warnings(fd))
if flag_issue:
assert len(result) == 1
assert result[0][0] == 1
assert result[0][1].startswith(f"Don't use {category}")
else:
assert len(result) == 0
53 changes: 53 additions & 0 deletions scripts/validate_unwanted_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
Callable,
Iterable,
)
import re
import sys
import token
import tokenize
from typing import IO

DEPRECATION_WARNINGS_PATTERN = re.compile(
r"(PendingDeprecation|Deprecation|Future)Warning"
)
PRIVATE_IMPORTS_TO_IGNORE: set[str] = {
"_extension_array_shared_docs",
"_index_shared_docs",
Expand Down Expand Up @@ -344,6 +348,54 @@ def nodefault_used_not_only_for_typing(file_obj: IO[str]) -> Iterable[tuple[int,
if isinstance(value, ast.AST)
)

def doesnt_use_pandas_warnings(file_obj: IO[str]) -> Iterable[tuple[int, str]]:
"""
Checking that pandas-specific warnings are used for deprecations.

Parameters
----------
file_obj : IO
File-like object containing the Python code to validate.

Yields
------
line_number : int
Line number of the warning.
msg : str
Explanation of the error.
"""
contents = file_obj.read()
lines = contents.split("\n")
tree = ast.parse(contents)
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue

if isinstance(node.func, ast.Attribute):
if node.func.attr != "warn":
continue
elif isinstance(node.func, ast.Name):
if node.func.id != "warn":
continue
if any(
"# pdlint: ignore[warning_class]" in lines[k]
for k in range(node.lineno - 1, node.end_lineno + 1)
):
continue
values = (
[arg.id for arg in node.args if isinstance(arg, ast.Name)]
+ [kw.value.id for kw in node.keywords if kw.arg == "category"]
)
for value in values:
matches = re.match(DEPRECATION_WARNINGS_PATTERN, value)
if matches is not None:
yield (
node.lineno,
f"Don't use {matches[0]}, use a pandas-specific warning in "
f"pd.errors instead. You can add "
f"`# pdlint: ignore[warning_class]` to override."
)


def main(
function: Callable[[IO[str]], Iterable[tuple[int, str]]],
Expand Down Expand Up @@ -397,6 +449,7 @@ def main(
"private_import_across_module",
"strings_with_wrong_placed_whitespace",
"nodefault_used_not_only_for_typing",
"doesnt_use_pandas_warnings",
]

parser = argparse.ArgumentParser(description="Unwanted patterns checker.")
Expand Down
Loading