Skip to content

Commit 6b60a27

Browse files
Charlie-XIAOim-vinicius
authored and
im-vinicius
committed
CI: linting check to ensure lib.NoDefault is only used for typing (pandas-dev#53901)
* CI add liniting to check NoDefault used only for typing * minor modification * reduce cost * tests added for the new linting check * types_or -> types because only python * rephrase pre-commit hook name * rephrase more * fix failing tests: * retrigger checks * retrigger checks
1 parent 0800f80 commit 6b60a27

File tree

3 files changed

+122
-1
lines changed

3 files changed

+122
-1
lines changed

.pre-commit-config.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,11 @@ repos:
339339
language: python
340340
entry: python scripts/validate_unwanted_patterns.py --validation-type="strings_with_wrong_placed_whitespace"
341341
types_or: [python, cython]
342+
- id: unwanted-patterns-nodefault-used-not-only-for-typing
343+
name: Check that `pandas._libs.lib.NoDefault` is used only for typing
344+
language: python
345+
entry: python scripts/validate_unwanted_patterns.py --validation-type="nodefault_used_not_only_for_typing"
346+
types: [python]
342347
- id: use-pd_array-in-core
343348
name: Import pandas.array as pd_array in core
344349
language: python

scripts/tests/test_validate_unwanted_patterns.py

+69
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,72 @@ def test_strings_with_wrong_placed_whitespace_raises(self, data, expected):
375375
validate_unwanted_patterns.strings_with_wrong_placed_whitespace(fd)
376376
)
377377
assert result == expected
378+
379+
380+
class TestNoDefaultUsedNotOnlyForTyping:
381+
@pytest.mark.parametrize(
382+
"data",
383+
[
384+
(
385+
"""
386+
def f(
387+
a: int | NoDefault,
388+
b: float | lib.NoDefault = 0.1,
389+
c: pandas._libs.lib.NoDefault = lib.no_default,
390+
) -> lib.NoDefault | None:
391+
pass
392+
"""
393+
),
394+
(
395+
"""
396+
# var = lib.NoDefault
397+
# the above is incorrect
398+
a: NoDefault | int
399+
b: lib.NoDefault = lib.no_default
400+
"""
401+
),
402+
],
403+
)
404+
def test_nodefault_used_not_only_for_typing(self, data):
405+
fd = io.StringIO(data.strip())
406+
result = list(validate_unwanted_patterns.nodefault_used_not_only_for_typing(fd))
407+
assert result == []
408+
409+
@pytest.mark.parametrize(
410+
"data, expected",
411+
[
412+
(
413+
(
414+
"""
415+
def f(
416+
a = lib.NoDefault,
417+
b: Any
418+
= pandas._libs.lib.NoDefault,
419+
):
420+
pass
421+
"""
422+
),
423+
[
424+
(2, "NoDefault is used not only for typing"),
425+
(4, "NoDefault is used not only for typing"),
426+
],
427+
),
428+
(
429+
(
430+
"""
431+
a: Any = lib.NoDefault
432+
if a is NoDefault:
433+
pass
434+
"""
435+
),
436+
[
437+
(1, "NoDefault is used not only for typing"),
438+
(2, "NoDefault is used not only for typing"),
439+
],
440+
),
441+
],
442+
)
443+
def test_nodefault_used_not_only_for_typing_raises(self, data, expected):
444+
fd = io.StringIO(data.strip())
445+
result = list(validate_unwanted_patterns.nodefault_used_not_only_for_typing(fd))
446+
assert result == expected

scripts/validate_unwanted_patterns.py

+48-1
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,52 @@ def has_wrong_whitespace(first_line: str, second_line: str) -> bool:
353353
)
354354

355355

356+
def nodefault_used_not_only_for_typing(file_obj: IO[str]) -> Iterable[Tuple[int, str]]:
357+
"""Test case where pandas._libs.lib.NoDefault is not used for typing.
358+
359+
Parameters
360+
----------
361+
file_obj : IO
362+
File-like object containing the Python code to validate.
363+
364+
Yields
365+
------
366+
line_number : int
367+
Line number of misused lib.NoDefault.
368+
msg : str
369+
Explanation of the error.
370+
"""
371+
contents = file_obj.read()
372+
tree = ast.parse(contents)
373+
in_annotation = False
374+
nodes: List[tuple[bool, ast.AST]] = [(in_annotation, tree)]
375+
376+
while nodes:
377+
in_annotation, node = nodes.pop()
378+
if not in_annotation and (
379+
isinstance(node, ast.Name) # Case `NoDefault`
380+
and node.id == "NoDefault"
381+
or isinstance(node, ast.Attribute) # Cases e.g. `lib.NoDefault`
382+
and node.attr == "NoDefault"
383+
):
384+
yield (node.lineno, "NoDefault is used not only for typing")
385+
386+
# This part is adapted from
387+
# https://github.com/asottile/pyupgrade/blob/5495a248f2165941c5d3b82ac3226ba7ad1fa59d/pyupgrade/_data.py#L70-L113
388+
for name in reversed(node._fields):
389+
value = getattr(node, name)
390+
if name in {"annotation", "returns"}:
391+
next_in_annotation = True
392+
else:
393+
next_in_annotation = in_annotation
394+
if isinstance(value, ast.AST):
395+
nodes.append((next_in_annotation, value))
396+
elif isinstance(value, list):
397+
for value in reversed(value):
398+
if isinstance(value, ast.AST):
399+
nodes.append((next_in_annotation, value))
400+
401+
356402
def main(
357403
function: Callable[[IO[str]], Iterable[Tuple[int, str]]],
358404
source_path: str,
@@ -405,6 +451,7 @@ def main(
405451
"private_function_across_module",
406452
"private_import_across_module",
407453
"strings_with_wrong_placed_whitespace",
454+
"nodefault_used_not_only_for_typing",
408455
]
409456

410457
parser = argparse.ArgumentParser(description="Unwanted patterns checker.")
@@ -413,7 +460,7 @@ def main(
413460
parser.add_argument(
414461
"--format",
415462
"-f",
416-
default="{source_path}:{line_number}:{msg}",
463+
default="{source_path}:{line_number}: {msg}",
417464
help="Output format of the error message.",
418465
)
419466
parser.add_argument(

0 commit comments

Comments
 (0)