Skip to content

TST: Use fixtures instead of setup_method for index tests #28865

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 3 commits into from
Oct 11, 2019

Conversation

jschendel
Copy link
Member

The common index tests use setup_method to create a dict of indexes to test against:

def setup_method(self, method):
self.indices = dict(
index=RangeIndex(0, 20, 2, name="foo"),
index_dec=RangeIndex(18, -1, -2, name="bar"),
)
self.setup_indices()

This dict of indexes is then iterated over within the tests:

def test_numpy_argsort(self):
for k, ind in self.indices.items():
result = np.argsort(ind)
expected = ind.argsort()
tm.assert_numpy_array_equal(result, expected)

The bulk of this PR involves converting self.indices into a parametrized fixture of indexes, and adjusting the tests to support this (largely just unindenting). I had to do this conversion for all indexes at once since common test code for all index classes utilizes this pattern, so the diff is fairly large, but it should be relatively simple changes.

I also had to make some changes to references to specific indexes as well (e.g. self.index, self.strIndex, etc.) since the setup_method code also directly set each index in the dict as a class attribute.

@jschendel jschendel added Refactor Internal refactoring of code Testing pandas testing functions or related to the test suite Index Related to the Index class or subclasses labels Oct 9, 2019
@jschendel jschendel added this to the 1.0 milestone Oct 9, 2019
@@ -58,13 +58,14 @@ def test_view(self):
tm.assert_index_equal(result, i_view)

def test_map_callable(self):
expected = self.index + self.index.freq
result = self.index.map(lambda x: x + x.freq)
index = self.create_index()
Copy link
Member Author

Choose a reason for hiding this comment

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

Planning to convert self.create_index() into a fixture in a follow up PR; in this PR I've used it to replace references to indexes set via setup_method.

Copy link
Member

Choose a reason for hiding this comment

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

i'm probably not be following the logic correctly, but self.create_index() is a different index to self.index for the datetimelike tests?

i.e. tdi

    def setup_method(self, method):
        self.indices = dict(index=tm.makeTimedeltaIndex(10))
        self.setup_indices()

so self.index is created with self.setup_indices() and not the same as

    def create_index(self):
        return pd.to_timedelta(range(5), unit="d") + pd.offsets.Hour(1)

import pandas.util.testing as tm
from pandas.util.testing import assert_almost_equal


class TestIndex(Base):
_holder = Index

def setup_method(self, method):
self.indices = dict(
Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't create an indices fixture for this class since there was already an identical indices fixture in conftest.py, so sharing that one here. I updated the conftest.py fixture to use the same naming conventions converted to snake_case, e.g. unicodeIndex --> unicode_index.

)
self.setup_indices()
@pytest.fixture
def index(self, request):
Copy link
Member Author

@jschendel jschendel Oct 9, 2019

Choose a reason for hiding this comment

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

I think this is pretty much the only non-standard thing I had to do. A lot of the tests in this file only tested a subset of the indexes in indices, so I needed a way to mimic that with fixtures. The idea here is to use indirect parametrization to allow this behavior, e.g.

@pytest.mark.parametrize("index", ["int", "uint", "float"], indirect=True)

Would result in this fixture returning the three indexes keyed by "int", "uint", "float" in indices_dict.

Copy link
Member

Choose a reason for hiding this comment

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

I think this is pretty much the only non-standard thing I had to do

this is OK. used in io.formats.test_to_html for the biggie_df_fixture.

the alternative is to create a fixture yielding just the ids which is then requested by the indices fixture. (see filepath_or_buffer_id and filepath_or_buffer fixtures in io.formats.test_format.) and then override the id fixture directly at function, class or module level.

both patterns (direct and indirect parameterisation) are equivalent.

Copy link
Member

Choose a reason for hiding this comment

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

looking at this in detail. I don't think you need to create a separate fixture in order to do this. you could apply indirect parametrisation to the indices fixture.

Copy link
Member

@simonjayhawkins simonjayhawkins left a comment

Choose a reason for hiding this comment

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

@jschendel Very Nice!

a few comments, but mostly can probably be done as a follow-on.

not finished reviewing all the changes yet. hopefully can get back to it in a few hours.

# gh-12309: Check that the "name" argument
# passed at initialization is honored.
if isinstance(indices, MultiIndex):
return
Copy link
Member

Choose a reason for hiding this comment

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

maybe this should be a pytest.skip instead of a return?

}


@pytest.fixture(params=indices_dict.keys())
def indices(request):
Copy link
Member

Choose a reason for hiding this comment

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

maybe a smaller diff if the fixture was named index?

Not just this PR, but in general maybe we shouldn't use the plural when creating fixtures since when the fixture is used it creates a single value at a time and so the plural name in the tests can sometimes be confusing.

with pytest.raises(InvalidIndexError, match=e):
index.get_indexer(index[0:2])
if isinstance(indices, IntervalIndex):
return
Copy link
Member

Choose a reason for hiding this comment

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

pytest.skip?

elif isinstance(indices, (RangeIndex, MultiIndex, CategoricalIndex)):
# RangeIndex cannot be initialized from data
# MultiIndex and CategoricalIndex are tested separately
return
Copy link
Member

Choose a reason for hiding this comment

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

pytest.skip?

)
elif isinstance(indices, IntervalIndex):
# checked in test_interval.py
pass
Copy link
Member

Choose a reason for hiding this comment

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

pytest.skip?

values = np.asarray(idx.values)

if len(indices) == 0:
return
Copy link
Member

Choose a reason for hiding this comment

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

skip?

elif isinstance(indices, DatetimeIndexOpsMixin):
values[1] = iNaT
elif isinstance(indices, (Int64Index, UInt64Index)):
return
Copy link
Member

Choose a reason for hiding this comment

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

skip?

if isinstance(indices, DatetimeIndexOpsMixin):
values[1] = iNaT
elif isinstance(indices, (Int64Index, UInt64Index)):
return
Copy link
Member

Choose a reason for hiding this comment

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

skip?

with pytest.raises(NotImplementedError, match=msg):
idx.fillna(idx[0])
if len(indices) == 0:
pass
Copy link
Member

Choose a reason for hiding this comment

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

skip?

@@ -58,13 +58,14 @@ def test_view(self):
tm.assert_index_equal(result, i_view)

def test_map_callable(self):
expected = self.index + self.index.freq
result = self.index.map(lambda x: x + x.freq)
index = self.create_index()
Copy link
Member

Choose a reason for hiding this comment

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

i'm probably not be following the logic correctly, but self.create_index() is a different index to self.index for the datetimelike tests?

i.e. tdi

    def setup_method(self, method):
        self.indices = dict(index=tm.makeTimedeltaIndex(10))
        self.setup_indices()

so self.index is created with self.setup_indices() and not the same as

    def create_index(self):
        return pd.to_timedelta(range(5), unit="d") + pd.offsets.Hour(1)

Copy link
Member

@simonjayhawkins simonjayhawkins left a comment

Choose a reason for hiding this comment

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

some more comments, again many could be a follow-on.

still have more files to review. see what others think but maybe this PR needs to be done in smaller chunks.

@@ -41,80 +41,65 @@
from pandas.core.indexes.api import Index, MultiIndex
from pandas.core.sorting import safe_sort
from pandas.tests.indexes.common import Base
from pandas.tests.indexes.conftest import indices_dict
Copy link
Member

Choose a reason for hiding this comment

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

we shouldn't really be importing from a conftest file. probably best to move indices_dict to common and then import from there.

)
self.setup_indices()
@pytest.fixture
def index(self, request):
Copy link
Member

Choose a reason for hiding this comment

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

looking at this in detail. I don't think you need to create a separate fixture in order to do this. you could apply indirect parametrisation to the indices fixture.

new_index = self.dateIndex[None, :]
@pytest.mark.parametrize("index", ["datetime"], indirect=True)
def test_new_axis(self, index):
new_index = index[None, :]
Copy link
Member

Choose a reason for hiding this comment

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

here and elsewhere I think result may be more appropriate than new_index

@@ -570,37 +555,50 @@ def test_constructor_cast(self):
with pytest.raises(ValueError, match=msg):
Index(["a", "b", "c"], dtype=float)

def test_view_with_args(self):
restricted = ["unicodeIndex", "strIndex", "catIndex", "boolIndex", "empty"]
Copy link
Member

Choose a reason for hiding this comment

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

it might be better to keep this and just do a pytest.skip if in restricted.

d = self.dateIndex[0]
assert self.dateIndex.asof(d) == d
assert isna(self.dateIndex.asof(d - timedelta(1)))
@pytest.mark.parametrize("index", ["datetime"], indirect=True)
Copy link
Member

Choose a reason for hiding this comment

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

tests of a single index type probably don't belong in base.

index = self.indices[name]
expected = Index(np.arange(len(index), 0, -1))

@pytest.mark.parametrize(
Copy link
Member

Choose a reason for hiding this comment

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

rather than duplicating the parameterisation, is it worth making mapper a fixture?

Comment on lines +1446 to +1447
def _check_method_works(self, method, index):
method(index)
Copy link
Member

Choose a reason for hiding this comment

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

only used twice, probably best to inline.

@@ -1402,7 +1408,7 @@ def test_format(self):
expected = [str(index[0])]
assert formatted == expected

self.strIndex[:0].format()
Index([]).format()
Copy link
Member

Choose a reason for hiding this comment

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

why is this changing?

tm.assert_index_equal(dropped, expected)

@pytest.mark.parametrize("index", ["string", "int", "float"], indirect=True)
Copy link
Member

Choose a reason for hiding this comment

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

if this combination is common could create an alias to the mark.

index.contains(1)
def test_deprecated_contains(self, indices):
# deprecated for all types except IntervalIndex
warning = FutureWarning if not isinstance(indices, pd.IntervalIndex) else None
Copy link
Member

Choose a reason for hiding this comment

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

may be more appropriate to skip on IntervalIndex

@jreback
Copy link
Contributor

jreback commented Oct 11, 2019

lgtm. @simonjayhawkins ok here?

@simonjayhawkins
Copy link
Member

lgtm. @simonjayhawkins ok here?

@jreback I think good to merge and tidy up as a folow-up.

@jreback jreback merged commit 981299c into pandas-dev:master Oct 11, 2019
@jreback
Copy link
Contributor

jreback commented Oct 11, 2019

thanks @simonjayhawkins agreed

nice @jschendel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Index Related to the Index class or subclasses Refactor Internal refactoring of code Testing pandas testing functions or related to the test suite
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants