Skip to content

Commit 90f72c3

Browse files
committed
CLN: Lint for usage of Deprecation/FutureWarning
1 parent 188b2da commit 90f72c3

File tree

8 files changed

+92
-7
lines changed

8 files changed

+92
-7
lines changed

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,11 @@ repos:
266266
language: python
267267
entry: python scripts/validate_unwanted_patterns.py --validation-type="nodefault_used_not_only_for_typing"
268268
types: [python]
269+
- id: unwanted-patterns-doesnt-use-pandas-warnings
270+
name: Check that warning classes for deprecations use pandas' warning classes
271+
language: python
272+
entry: python scripts/validate_unwanted_patterns.py --validation-type="doesnt_use_pandas_warnings"
273+
types: [ python ]
269274
- id: no-return-exception
270275
name: Use raise instead of return for exceptions
271276
language: pygrep

pandas/core/generic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9138,7 +9138,7 @@ def resample(
91389138
"deprecated and will be removed in a future version. "
91399139
"Explicitly cast PeriodIndex to DatetimeIndex before resampling "
91409140
"instead.",
9141-
FutureWarning,
9141+
FutureWarning, # pdlint: ignore
91429142
stacklevel=find_stack_level(),
91439143
)
91449144
else:

pandas/core/groupby/groupby.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ def groups(self) -> dict[Hashable, Index]:
557557
"and will be removed. In a future version `groups` by one element "
558558
"list will return tuple. Use ``df.groupby(by='a').groups`` "
559559
"instead of ``df.groupby(by=['a']).groups`` to avoid this warning",
560-
FutureWarning,
560+
FutureWarning, # pdlint: ignore
561561
stacklevel=find_stack_level(),
562562
)
563563
return self._grouper.groups

pandas/core/internals/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __getattr__(name: str):
2525
warnings.warn(
2626
f"{name} is deprecated and will be removed in a future version. "
2727
"Use public APIs instead.",
28-
FutureWarning,
28+
FutureWarning, # pdlint: ignore
2929
# https://github.com/pandas-dev/pandas/pull/55139#pullrequestreview-1720690758
3030
# on hard-coding stacklevel
3131
stacklevel=2,

pandas/core/series.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4435,7 +4435,7 @@ def map(
44354435
warnings.warn(
44364436
"The parameter `arg` has been renamed to `func`, and it "
44374437
"will stop being supported in a future version of pandas.",
4438-
FutureWarning,
4438+
FutureWarning, # pdlint: ignore
44394439
stacklevel=find_stack_level(),
44404440
)
44414441
else:

pandas/core/strings/object_array.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def _str_contains(
163163
warnings.warn(
164164
"Allowing a non-bool 'na' in obj.str.contains is deprecated "
165165
"and will raise in a future version.",
166-
FutureWarning,
166+
FutureWarning, # pdlint: ignore
167167
stacklevel=find_stack_level(),
168168
)
169169
return self._str_map(f, na, dtype=np.dtype("bool"))
@@ -175,7 +175,7 @@ def _str_startswith(self, pat, na=lib.no_default):
175175
warnings.warn(
176176
"Allowing a non-bool 'na' in obj.str.startswith is deprecated "
177177
"and will raise in a future version.",
178-
FutureWarning,
178+
FutureWarning, # pdlint: ignore
179179
stacklevel=find_stack_level(),
180180
)
181181
return self._str_map(f, na_value=na, dtype=np.dtype(bool))
@@ -187,7 +187,7 @@ def _str_endswith(self, pat, na=lib.no_default):
187187
warnings.warn(
188188
"Allowing a non-bool 'na' in obj.str.endswith is deprecated "
189189
"and will raise in a future version.",
190-
FutureWarning,
190+
FutureWarning, # pdlint: ignore
191191
stacklevel=find_stack_level(),
192192
)
193193
return self._str_map(f, na_value=na, dtype=np.dtype(bool))

scripts/tests/test_validate_unwanted_patterns.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,37 @@ def test_nodefault_used_not_only_for_typing_raises(self, data, expected) -> None
296296
fd = io.StringIO(data.strip())
297297
result = list(validate_unwanted_patterns.nodefault_used_not_only_for_typing(fd))
298298
assert result == expected
299+
300+
301+
@pytest.mark.parametrize("function", ["warnings.warn", "warn"])
302+
@pytest.mark.parametrize("positional", [True, False])
303+
@pytest.mark.parametrize(
304+
"category",
305+
[
306+
"FutureWarning",
307+
"DeprecationWarning",
308+
"PendingDeprecationWarning",
309+
"Pandas4Warning",
310+
"RuntimeWarning"
311+
],
312+
)
313+
@pytest.mark.parametrize("pdlint_ignore", [True, False])
314+
315+
def test_doesnt_use_pandas_warnings(function, positional, category, pdlint_ignore):
316+
code = (
317+
f"{function}({' # pdlint: ignore[warning_class]' if pdlint_ignore else ''}\n"
318+
f' "message",\n'
319+
f" {'' if positional else 'category='}{category},\n"
320+
f")\n"
321+
)
322+
flag_issue = (
323+
category in ["FutureWarning", "DeprecationWarning", "PendingDeprecationWarning"]
324+
and not pdlint_ignore
325+
)
326+
fd = io.StringIO(code)
327+
result = list(validate_unwanted_patterns.doesnt_use_pandas_warnings(fd))
328+
if flag_issue:
329+
assert len(result) == 1
330+
assert result[0] == (1, f"Don't use {category}")
331+
else:
332+
assert len(result) == 0

scripts/validate_unwanted_patterns.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616
Callable,
1717
Iterable,
1818
)
19+
import re
1920
import sys
2021
import token
2122
import tokenize
2223
from typing import IO
2324

25+
DEPRECATION_WARNINGS_PATTERN = re.compile(
26+
r"(PendingDeprecation|Deprecation|Future)Warning"
27+
)
2428
PRIVATE_IMPORTS_TO_IGNORE: set[str] = {
2529
"_extension_array_shared_docs",
2630
"_index_shared_docs",
@@ -344,6 +348,47 @@ def nodefault_used_not_only_for_typing(file_obj: IO[str]) -> Iterable[tuple[int,
344348
if isinstance(value, ast.AST)
345349
)
346350

351+
def doesnt_use_pandas_warnings(file_obj: IO[str]) -> Iterable[tuple[int, str]]:
352+
"""
353+
Checking that a private function is not used across modules.
354+
Parameters
355+
----------
356+
file_obj : IO
357+
File-like object containing the Python code to validate.
358+
Yields
359+
------
360+
line_number : int
361+
Line number of the private function that is used across modules.
362+
msg : str
363+
Explanation of the error.
364+
"""
365+
contents = file_obj.read()
366+
lines = contents.split("\n")
367+
tree = ast.parse(contents)
368+
for node in ast.walk(tree):
369+
if not isinstance(node, ast.Call):
370+
continue
371+
372+
if isinstance(node.func, ast.Attribute):
373+
if node.func.attr != "warn":
374+
continue
375+
elif isinstance(node.func, ast.Name):
376+
if node.func.id != "warn":
377+
continue
378+
if any(
379+
"# pdlint: ignore[warning_class]" in lines[k]
380+
for k in range(node.lineno - 1, node.end_lineno + 1)
381+
):
382+
continue
383+
values = (
384+
[arg.id for arg in node.args if isinstance(arg, ast.Name)]
385+
+ [kw.value.id for kw in node.keywords if kw.arg == "category"]
386+
)
387+
for value in values:
388+
matches = re.match(DEPRECATION_WARNINGS_PATTERN, value)
389+
if matches is not None:
390+
yield (node.lineno, f"Don't use {matches[0]}")
391+
347392

348393
def main(
349394
function: Callable[[IO[str]], Iterable[tuple[int, str]]],
@@ -397,6 +442,7 @@ def main(
397442
"private_import_across_module",
398443
"strings_with_wrong_placed_whitespace",
399444
"nodefault_used_not_only_for_typing",
445+
"doesnt_use_pandas_warnings",
400446
]
401447

402448
parser = argparse.ArgumentParser(description="Unwanted patterns checker.")

0 commit comments

Comments
 (0)