Skip to content

STYLE pre-commit check to ensure that test functions name starts with test #50397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,13 @@ repos:
additional_dependencies:
- autotyping==22.9.0
- libcst==0.4.7
- id: check-test-naming
name: check that test names start with 'test'
entry: python -m scripts.check_test_naming
types: [python]
files: ^pandas/tests
language: python
exclude: |
(?x)
^pandas/tests/generic/test_generic.py # GH50380
|^pandas/tests/io/json/test_readlines.py # GH50378
Comment on lines +344 to +345
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these exclusions will be removed as they're addressed

2 changes: 1 addition & 1 deletion pandas/tests/computation/test_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ def test_pow(self, lhs, rhs, engine, parser):
expected = _eval_single_bin(middle, "**", rhs, engine)
tm.assert_almost_equal(result, expected)

def check_single_invert_op(self, lhs, engine, parser):
def test_check_single_invert_op(self, lhs, engine, parser):
# simple
try:
elb = lhs.astype(bool)
Expand Down
7 changes: 0 additions & 7 deletions pandas/tests/frame/methods/test_dtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,6 @@
import pandas._testing as tm


def _check_cast(df, v):
"""
Check if all dtypes of df are equal to v
"""
assert all(s.dtype.name == v for _, s in df.items())


class TestDataFrameDataTypes:
def test_empty_frame_dtypes(self):
empty_df = DataFrame()
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/frame/methods/test_to_timestamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_to_timestamp_columns(self):
assert result1.columns.freqstr == "AS-JAN"
assert result2.columns.freqstr == "AS-JAN"

def to_timestamp_invalid_axis(self):
def test_to_timestamp_invalid_axis(self):
index = period_range(freq="A", start="1/1/2001", end="12/1/2009")
obj = DataFrame(np.random.randn(len(index), 5), index=index)

Expand Down
21 changes: 0 additions & 21 deletions pandas/tests/internals/test_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -1323,10 +1323,6 @@ def test_period_can_hold_element(self, element):
elem = element(dti)
self.check_series_setitem(elem, pi, False)

def check_setting(self, elem, index: Index, inplace: bool):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

helper function which wasn't being used anymore

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbrockmendel looks like it was added in https://github.com/pandas-dev/pandas/pull/39120/files - do you remember if it was meant to be used anywhere?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it was meant to de-duplicate something but never got used. Rip it out!

self.check_series_setitem(elem, index, inplace)
self.check_frame_setitem(elem, index, inplace)

def check_can_hold_element(self, obj, elem, inplace: bool):
blk = obj._mgr.blocks[0]
if inplace:
Expand All @@ -1350,23 +1346,6 @@ def check_series_setitem(self, elem, index: Index, inplace: bool):
else:
assert ser.dtype == object

def check_frame_setitem(self, elem, index: Index, inplace: bool):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

helper function which wasn't being used anymore

arr = index._data.copy()
df = DataFrame(arr)

self.check_can_hold_element(df, elem, inplace)

if is_scalar(elem):
df.iloc[0, 0] = elem
else:
df.iloc[: len(elem), 0] = elem

if inplace:
# assertion here implies setting was done inplace
assert df._mgr.arrays[0] is arr
else:
assert df.dtypes[0] == object


class TestShouldStore:
def test_should_store_categorical(self):
Expand Down
5 changes: 3 additions & 2 deletions pandas/tests/io/test_feather.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@ def test_read_columns(self):
columns = ["col1", "col3"]
self.check_round_trip(df, expected=df[columns], columns=columns)

def read_columns_different_order(self):
def test_read_columns_different_order(self):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alimcmaster1 this test was never running because its name didn't start with test_ (just FYI, no blame!)

# GH 33878
df = pd.DataFrame({"A": [1, 2], "B": ["x", "y"], "C": [True, False]})
self.check_round_trip(df, columns=["B", "A"])
expected = df[["B", "A"]]
self.check_round_trip(df, expected, columns=["B", "A"])

def test_unsupported_other(self):

Expand Down
15 changes: 0 additions & 15 deletions pandas/tests/reshape/concat/test_append_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,6 @@ def item(self, request):

item2 = item

def _check_expected_dtype(self, obj, label):
"""
Check whether obj has expected dtype depending on label
considering not-supported dtypes
"""
if isinstance(obj, Index):
assert obj.dtype == label
elif isinstance(obj, Series):
if label.startswith("period"):
assert obj.dtype == "Period[M]"
else:
assert obj.dtype == label
else:
raise ValueError

def test_dtypes(self, item, index_or_series):
# to confirm test case covers intended dtypes
typ, vals = item
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/series/methods/test_explode.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_invert_array():
@pytest.mark.parametrize(
"s", [pd.Series([1, 2, 3]), pd.Series(pd.date_range("2019", periods=3, tz="UTC"))]
)
def non_object_dtype(s):
def test_non_object_dtype(s):
result = s.explode()
tm.assert_series_equal(result, s)

Expand Down
8 changes: 7 additions & 1 deletion pandas/tests/strings/test_cat.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
_testing as tm,
concat,
)
from pandas.tests.strings.test_strings import assert_series_or_index_equal
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function was defined in another file, but only used here. so, just moving it here



def assert_series_or_index_equal(left, right):
if isinstance(left, Series):
tm.assert_series_equal(left, right)
else: # Index
tm.assert_index_equal(left, right)


@pytest.mark.parametrize("other", [None, Series, Index])
Expand Down
7 changes: 0 additions & 7 deletions pandas/tests/strings/test_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,6 @@ def test_startswith_endswith_non_str_patterns(pattern):
ser.str.endswith(pattern)


def assert_series_or_index_equal(left, right):
if isinstance(left, Series):
tm.assert_series_equal(left, right)
else: # Index
tm.assert_index_equal(left, right)


# test integer/float dtypes (inferred by constructor) and mixed


Expand Down
7 changes: 6 additions & 1 deletion pandas/tests/tseries/offsets/test_dst.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@
YearEnd,
)

from pandas.tests.tseries.offsets.test_offsets import get_utc_offset_hours
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defined in another file, but only used here. so, let's just define it here

from pandas.util.version import Version

# error: Module has no attribute "__version__"
pytz_version = Version(pytz.__version__) # type: ignore[attr-defined]


def get_utc_offset_hours(ts):
# take a Timestamp and compute total hours of utc offset
o = ts.utcoffset()
return (o.days * 24 * 3600 + o.seconds) / 3600.0


class TestDST:

# one microsecond before the DST transition
Expand Down
6 changes: 0 additions & 6 deletions pandas/tests/tseries/offsets/test_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,12 +900,6 @@ def test_str_for_named_is_name(self):
assert offset.freqstr == name


def get_utc_offset_hours(ts):
# take a Timestamp and compute total hours of utc offset
o = ts.utcoffset()
return (o.days * 24 * 3600 + o.seconds) / 3600.0


# ---------------------------------------------------------------------


Expand Down
18 changes: 0 additions & 18 deletions pandas/tests/util/test_assert_frame_equal.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,6 @@ def _assert_frame_equal_both(a, b, **kwargs):
tm.assert_frame_equal(b, a, **kwargs)


def _assert_not_frame_equal(a, b, **kwargs):
"""
Check that two DataFrame are not equal.

Parameters
----------
a : DataFrame
The first DataFrame to compare.
b : DataFrame
The second DataFrame to compare.
kwargs : dict
The arguments passed to `tm.assert_frame_equal`.
"""
msg = "The two DataFrames were equal when they shouldn't have been"
with pytest.raises(AssertionError, match=msg):
tm.assert_frame_equal(a, b, **kwargs)


@pytest.mark.parametrize("check_like", [True, False])
def test_frame_equal_row_order_mismatch(check_like, obj_fixture):
df1 = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, index=["a", "b", "c"])
Expand Down
152 changes: 152 additions & 0 deletions scripts/check_test_naming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""
Check that test names start with `test`, and that test classes start with `Test`.

This is meant to be run as a pre-commit hook - to run it manually, you can do:

pre-commit run check-test-naming --all-files

NOTE: if this finds a false positive, you can add the comment `# not a test` to the
class or function definition. Though hopefully that shouldn't be necessary.
"""
from __future__ import annotations

import argparse
import ast
import os
from pathlib import Path
import sys
from typing import (
Iterator,
Sequence,
)

PRAGMA = "# not a test"


def _find_names(node: ast.Module) -> Iterator[str]:
for _node in ast.walk(node):
if isinstance(_node, ast.Name):
yield _node.id
elif isinstance(_node, ast.Attribute):
yield _node.attr


def _is_fixture(node: ast.expr) -> bool:
if isinstance(node, ast.Call):
node = node.func
return (
isinstance(node, ast.Attribute)
and node.attr == "fixture"
and isinstance(node.value, ast.Name)
and node.value.id == "pytest"
)


def _is_register_dtype(node):
return isinstance(node, ast.Name) and node.id == "register_extension_dtype"


def is_misnamed_test_func(
node: ast.expr | ast.stmt, names: Sequence[str], line: str
) -> bool:
return (
isinstance(node, ast.FunctionDef)
and not node.name.startswith("test")
and names.count(node.name) == 0
and not any(_is_fixture(decorator) for decorator in node.decorator_list)
and PRAGMA not in line
and node.name
not in ("teardown_method", "setup_method", "teardown_class", "setup_class")
)


def is_misnamed_test_class(
node: ast.expr | ast.stmt, names: Sequence[str], line: str
) -> bool:
return (
isinstance(node, ast.ClassDef)
and not node.name.startswith("Test")
and names.count(node.name) == 0
and not any(_is_register_dtype(decorator) for decorator in node.decorator_list)
and PRAGMA not in line
)


def main(content: str, file: str) -> int:
lines = content.splitlines()
tree = ast.parse(content)
names = list(_find_names(tree))
ret = 0
for node in tree.body:
if is_misnamed_test_func(node, names, lines[node.lineno - 1]):
print(
f"{file}:{node.lineno}:{node.col_offset} "
"found test function which does not start with 'test'"
)
ret = 1
elif is_misnamed_test_class(node, names, lines[node.lineno - 1]):
print(
f"{file}:{node.lineno}:{node.col_offset} "
"found test class which does not start with 'Test'"
)
ret = 1
if (
isinstance(node, ast.ClassDef)
and names.count(node.name) == 0
and not any(
_is_register_dtype(decorator) for decorator in node.decorator_list
)
and PRAGMA not in lines[node.lineno - 1]
):
for _node in node.body:
if is_misnamed_test_func(_node, names, lines[_node.lineno - 1]):
# It could be that this function is used somewhere by the
# parent class. For example, there might be a base class
# with
#
# class Foo:
# def foo(self):
# assert 1+1==2
# def test_foo(self):
# self.foo()
#
# and then some subclass overwrites `foo`. So, we check that
# `self.foo` doesn't appear in any of the test classes.
# Note some false negatives might get through, but that's OK.
# This is good enough that has helped identify several examples
# of tests not being run.
assert isinstance(_node, ast.FunctionDef) # help mypy
should_continue = False
for _file in (Path("pandas") / "tests").rglob("*.py"):
with open(os.path.join(_file)) as fd:
_content = fd.read()
if f"self.{_node.name}" in _content:
should_continue = True
break
if should_continue:
continue

print(
f"{file}:{_node.lineno}:{_node.col_offset} "
"found test function which does not start with 'test'"
)
ret = 1
return ret


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("paths", nargs="*")
args = parser.parse_args()

ret = 0

for file in args.paths:
filename = os.path.basename(file)
if not (filename.startswith("test") and filename.endswith(".py")):
continue
with open(file, encoding="utf-8") as fd:
content = fd.read()
ret |= main(content, file)

sys.exit(ret)
Loading