Skip to content

Commit 98a98c7

Browse files
committed
cleanup tests
1 parent 11e3b9b commit 98a98c7

18 files changed

Lines changed: 147 additions & 174 deletions

.github/copilot-instructions.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Example: `FPS` (Farthest Point Sampling) exists as both `feature_selection.FPS`
2424

2525
## Development Workflows
2626

27+
Use 88 character line length limit for code and docstrings.
28+
2729
### Testing
2830
```bash
2931
# Run all tests with coverage
@@ -39,11 +41,27 @@ tox -e tests-dev
3941
Tests use pytest-style assertions and fixtures. Common patterns:
4042
- Use `@pytest.fixture` for test data setup
4143
- Use `assert` statements instead of `self.assertEqual()`
42-
- Use `pytest.raises()` for exception testing
44+
- Use `pytest.raises()` for exception testing with `match=` parameter
4345
- Use `pytest.warns()` for warning testing
4446
- Use `pytest.mark.parametrize` for parameterized tests
4547
- Tests often load datasets via `skmatter.datasets.load_*()`
4648

49+
**Exception Testing Style:**
50+
- Keep `with pytest.raises(...)` statement on one line (88 char limit)
51+
- For long match strings, define a `match` variable before the with statement:
52+
```python
53+
match = "Long error message that would exceed line length limit"
54+
with pytest.raises(ValueError, match=match):
55+
some_function()
56+
```
57+
- Use `re.escape()` when matching messages with special regex characters:
58+
```python
59+
import re
60+
match = f"Found array with shape={X.shape} ..."
61+
with pytest.raises(ValueError, match=re.escape(match)):
62+
selector.fit(X)
63+
```
64+
4765
### Linting & Formatting
4866
```bash
4967
# Check only (CI mode)

tests/test_datasets.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,8 @@ def test_load_csd_1000r_access_descr(csd):
9797
def test_load_dataset_without_pandas():
9898
"""Check if the correct exception occurs when pandas isn't present."""
9999
with mock.patch.dict("sys.modules", {"pandas": None}):
100-
with pytest.raises(ImportError) as cm:
101-
_ = load_who_dataset()
102-
assert str(cm.value) == "load_who_dataset requires pandas."
100+
with pytest.raises(ImportError, match="load_who_dataset requires pandas."):
101+
load_who_dataset()
103102

104103

105104
def test_dataset_size_and_shape(who_data):

tests/test_dch.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,11 @@ def test_residual_features_ndim(test_data):
109109

110110
selector = DirectionalConvexHull()
111111
selector.fit(T, y)
112-
with pytest.raises(ValueError) as cm:
113-
selector.score_feature_matrix(X)
114-
assert str(cm.value) == (
112+
match = (
115113
"X has 10 features, but DirectionalConvexHull is expecting 4 features as input."
116114
)
115+
with pytest.raises(ValueError, match=match):
116+
selector.score_feature_matrix(X)
117117

118118

119119
def test_negative_score(test_data):

tests/test_feature_pcov_fps.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ def test_no_mixing_1(X_y_idx):
2929
"""Check that the model throws an error when mixing = 1.0."""
3030
X, y, _ = X_y_idx
3131
selector = PCovFPS(n_to_select=1, mixing=1.0)
32-
with pytest.raises(ValueError) as cm:
32+
match = "Mixing = 1.0 corresponds to traditional FPS. Please use the FPS class."
33+
with pytest.raises(ValueError, match=match):
3334
selector.fit(X, y=y)
34-
assert (
35-
str(cm.value)
36-
== "Mixing = 1.0 corresponds to traditional FPS. Please use the FPS class."
37-
)

tests/test_feature_simple_cur.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def X():
1515
def test_bad_transform(X):
1616
selector = CUR(n_to_select=2)
1717
with pytest.raises(exceptions.NotFittedError):
18-
_ = selector.transform(X)
18+
selector.transform(X)
1919

2020

2121
def test_restart(X):

tests/test_feature_simple_fps.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,21 @@ def test_initialize(X_and_idx):
5050
for i in range(4):
5151
assert selector.selected_idx_[i] == idx[i]
5252

53+
match = "Invalid value of the initialize parameter"
54+
5355
initialize = np.array([1, 5, 3, 0.25])
54-
with pytest.raises(ValueError) as cm:
56+
with pytest.raises(ValueError, match=match):
5557
selector = FPS(n_to_select=len(idx) - 1, initialize=initialize)
5658
selector.fit(X)
57-
assert str(cm.value) == "Invalid value of the initialize parameter"
5859

5960
initialize = np.array([[1, 5, 3], [2, 4, 6]])
60-
with pytest.raises(ValueError) as cm:
61+
with pytest.raises(ValueError, match=match):
6162
selector = FPS(n_to_select=len(idx) - 1, initialize=initialize)
6263
selector.fit(X)
63-
assert str(cm.value) == "Invalid value of the initialize parameter"
6464

65-
with pytest.raises(ValueError) as cm:
65+
with pytest.raises(ValueError, match=match):
6666
selector = FPS(n_to_select=1, initialize="bad")
6767
selector.fit(X)
68-
assert str(cm.value) == "Invalid value of the initialize parameter"
6968

7069

7170
def test_get_distances(X_and_idx):
@@ -80,7 +79,7 @@ def test_get_distances(X_and_idx):
8079

8180
with pytest.raises(NotFittedError):
8281
selector = FPS(n_to_select=7)
83-
_ = selector.get_select_distance()
82+
selector.get_select_distance()
8483

8584

8685
def test_unique_selected_idx_zero_score():

tests/test_greedy_selector.py

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
import numpy as np
24
import pytest
35
from sklearn.datasets import load_diabetes as get_dataset
@@ -30,10 +32,9 @@ def X():
3032

3133

3234
def test_bad_type(X):
33-
with pytest.raises(
34-
ValueError, match="Only feature and sample selection supported."
35-
):
36-
_ = GreedyTester(selection_type="bad").fit(X)
35+
match = "Only feature and sample selection supported."
36+
with pytest.raises(ValueError, match=match):
37+
GreedyTester(selection_type="bad").fit(X)
3738

3839

3940
def test_score_threshold(X):
@@ -46,28 +47,21 @@ def test_score_threshold(X):
4647

4748

4849
def test_score_threshold_and_full(X):
49-
with pytest.raises(ValueError) as cm:
50-
_ = GreedyTester(score_threshold=20, full=True, n_to_select=12).fit(X)
51-
assert str(cm.value) == "You cannot specify both `score_threshold` and `full=True`."
50+
match = "You cannot specify both `score_threshold` and `full=True`."
51+
with pytest.raises(ValueError, match=match):
52+
GreedyTester(score_threshold=20, full=True, n_to_select=12).fit(X)
5253

5354

5455
def test_bad_score_threshold_type(X):
55-
with pytest.raises(ValueError) as cm:
56-
_ = GreedyTester(score_threshold_type="bad").fit(X)
57-
assert (
58-
str(cm.value)
59-
== "invalid score_threshold_type, expected one of 'relative' or 'absolute'"
60-
)
56+
match = "invalid score_threshold_type, expected one of 'relative' or 'absolute'"
57+
with pytest.raises(ValueError, match=match):
58+
GreedyTester(score_threshold_type="bad").fit(X)
6159

6260

6361
def test_bad_warm_start(X):
6462
selector = GreedyTester()
65-
with pytest.raises(
66-
ValueError,
67-
match=(
68-
"Cannot fit with warm_start=True without having been previously initialized"
69-
),
70-
):
63+
match = "Cannot fit with warm_start=True without having been previously initialized"
64+
with pytest.raises(ValueError, match=match):
7165
selector.fit(X, warm_start=True)
7266

7367

@@ -82,12 +76,9 @@ def test_bad_y(X):
8276
def test_bad_transform(X):
8377
selector = GreedyTester(n_to_select=2)
8478
selector.fit(X)
85-
with pytest.raises(ValueError) as cm:
86-
_ = selector.transform(X[:, :3])
87-
assert (
88-
str(cm.value)
89-
== "X has 3 features, but GreedyTester is expecting 10 features as input."
90-
)
79+
match = "X has 3 features, but GreedyTester is expecting 10 features as input."
80+
with pytest.raises(ValueError, match=match):
81+
selector.transform(X[:, :3])
9182

9283

9384
def test_no_nfeatures(X):
@@ -105,27 +96,26 @@ def test_decimal_nfeatures(X):
10596
@pytest.mark.parametrize("nf", [1.2, "1", 20])
10697
def test_bad_nfeatures(X, nf):
10798
selector = GreedyTester(n_to_select=nf)
108-
with pytest.raises(ValueError) as cm:
109-
selector.fit(X)
110-
assert str(cm.value) == (
111-
"n_to_select must be either None, an integer in "
112-
"[1, n_features] representing the absolute number "
113-
"of features, or a float in (0, 1] representing a "
114-
f"percentage of features to select. Got {nf} "
115-
f"features and an input with {X.shape[1]} feature."
99+
expected_msg = (
100+
"n_to_select must be either None, an integer in [1, n_features] representing "
101+
"the absolute number of features, or a float in (0, 1] representing a "
102+
f"percentage of features to select. Got {nf} features and an input with "
103+
f"{X.shape[1]} feature."
116104
)
105+
with pytest.raises(ValueError, match=re.escape(expected_msg)):
106+
selector.fit(X)
117107

118108

119109
def test_not_fitted():
120110
with pytest.raises(NotFittedError):
121111
selector = GreedyTester()
122-
_ = selector._get_support_mask()
112+
selector._get_support_mask()
123113

124114

125115
def test_fitted(X):
126116
selector = GreedyTester()
127117
selector.fit(X)
128-
_ = selector._get_support_mask()
118+
selector._get_support_mask()
129119

130120
Xr = selector.transform(X)
131121
assert Xr.shape[1] == X.shape[1] // 2
@@ -135,18 +125,17 @@ def test_size_input():
135125
X = np.array([1, 2, 3, 4, 5]).reshape(-1, 1)
136126
selector_sample = GreedyTester(selection_type="sample")
137127
selector_feature = GreedyTester(selection_type="feature")
138-
with pytest.raises(ValueError) as cm:
139-
selector_feature.fit(X)
140-
assert str(cm.value) == (
128+
expected_msg_feature = (
141129
f"Found array with 1 feature(s) (shape={X.shape}) while a minimum of 2 is "
142130
"required by GreedyTester."
143131
)
132+
with pytest.raises(ValueError, match=re.escape(expected_msg_feature)):
133+
selector_feature.fit(X)
144134

145135
X = X.reshape(1, -1)
146-
147-
with pytest.raises(ValueError) as cm:
148-
selector_sample.fit(X)
149-
assert str(cm.value) == (
136+
expected_msg_sample = (
150137
f"Found array with 1 sample(s) (shape={X.shape}) while a minimum of 2 is "
151138
"required by GreedyTester."
152139
)
140+
with pytest.raises(ValueError, match=re.escape(expected_msg_sample)):
141+
selector_sample.fit(X)

tests/test_kernel_pcovc.py

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,14 @@ def test_reconstruction_errors(kpcovc_model, X, Y, error_tol):
110110
def test_nonfitted_failure(X):
111111
kpcovc = KernelPCovC(mixing=0.5, n_components=4, tol=1e-12)
112112
with pytest.raises(exceptions.NotFittedError):
113-
_ = kpcovc.transform(X)
113+
kpcovc.transform(X)
114114

115115

116116
def test_no_arg_predict(X, Y):
117117
kpcovc = KernelPCovC(mixing=0.5, n_components=4, tol=1e-12)
118118
kpcovc.fit(X, Y)
119119
with pytest.raises(ValueError):
120-
_ = kpcovc.predict()
120+
kpcovc.predict()
121121

122122

123123
def test_T_shape(X, Y):
@@ -145,28 +145,30 @@ def test_Z_shape(kpcovc_model, X, Y):
145145
def test_decision_function(kpcovc_model, X, Y):
146146
kpcovc = kpcovc_model(center=True)
147147
kpcovc.fit(X, Y)
148-
with pytest.raises(ValueError) as cm:
149-
_ = kpcovc.decision_function()
150-
assert str(cm.value) == "Either X or T must be supplied."
151-
_ = kpcovc.decision_function(X)
148+
149+
with pytest.raises(ValueError, match="Either X or T must be supplied."):
150+
kpcovc.decision_function()
151+
152+
kpcovc.decision_function(X)
152153
T = kpcovc.transform(X)
153-
_ = kpcovc.decision_function(T=T)
154+
kpcovc.decision_function(T=T)
154155

155156

156157
def test_no_centerer(kpcovc_model, X, Y):
157158
kpcovc = kpcovc_model(center=False)
158159
kpcovc.fit(X, Y)
159160
with pytest.raises(AttributeError):
160-
_ = kpcovc.centerer_
161+
kpcovc.centerer_
161162

162163

163164
def test_centerer(kpcovc_model, X, Y):
164165
kpcovc = kpcovc_model(center=True)
165166
kpcovc.fit(X, Y)
166167
assert hasattr(kpcovc, "centerer_")
167-
_ = kpcovc.predict(X)
168-
_ = kpcovc.transform(X)
169-
_ = kpcovc.score(X, Y)
168+
169+
kpcovc.predict(X)
170+
kpcovc.transform(X)
171+
kpcovc.score(X, Y)
170172

171173

172174
def test_prefit_classifier(X, Y):
@@ -198,14 +200,14 @@ def test_incompatible_classifier(kpcovc_model, X, Y):
198200
classifier = GaussianNB()
199201
classifier.fit(X, Y)
200202
kpcovc = kpcovc_model(mixing=0.5, classifier=classifier)
201-
with pytest.raises(ValueError) as cm:
202-
kpcovc.fit(X, Y)
203-
assert str(cm.value) == (
203+
expected_msg = (
204204
"Classifier must be an instance of "
205205
"`LogisticRegression`, `LogisticRegressionCV`, `LinearSVC`, "
206206
"`LinearDiscriminantAnalysis`, `RidgeClassifier`, `RidgeClassifierCV`, "
207207
"`SGDClassifier`, `Perceptron`, or `precomputed`"
208208
)
209+
with pytest.raises(ValueError, match=expected_msg):
210+
kpcovc.fit(X, Y)
209211

210212

211213
def test_none_classifier(X, Y):
@@ -221,15 +223,13 @@ def test_incompatible_coef_shape(kpcovc_model, X, Y):
221223
cl_multi = LinearSVC()
222224
cl_multi.fit(K, np.random.randint(0, 3, size=X.shape[0]))
223225
kpcovc_binary = kpcovc_model(mixing=0.5, classifier=cl_multi)
224-
with pytest.raises(ValueError) as cm:
226+
with pytest.raises(ValueError, match="For binary classification"):
225227
kpcovc_binary.fit(X, Y)
226-
assert "For binary classification" in str(cm.value)
227228
cl_binary = LinearSVC()
228229
cl_binary.fit(K, Y)
229230
kpcovc_multi = kpcovc_model(mixing=0.5, classifier=cl_binary)
230-
with pytest.raises(ValueError) as cm:
231+
with pytest.raises(ValueError, match="For multiclass classification"):
231232
kpcovc_multi.fit(X, np.random.randint(0, 3, size=X.shape[0]))
232-
assert "For multiclass classification" in str(cm.value)
233233

234234

235235
def test_precomputed_classification(X, Y, error_tol):
@@ -327,10 +327,9 @@ def test_svd_solvers(kpcovc_model, X, Y):
327327

328328

329329
def test_bad_solver(kpcovc_model, X, Y):
330-
with pytest.raises(ValueError) as cm:
330+
with pytest.raises(ValueError, match="Unrecognized svd_solver='bad'"):
331331
kpcovc = kpcovc_model(svd_solver="bad")
332332
kpcovc.fit(X, Y)
333-
assert str(cm.value) == "Unrecognized svd_solver='bad'"
334333

335334

336335
def test_good_n_components(kpcovc_model, X, Y):
@@ -344,20 +343,16 @@ def test_good_n_components(kpcovc_model, X, Y):
344343

345344

346345
def test_bad_n_components(kpcovc_model, X, Y):
347-
with pytest.raises(ValueError) as cm:
346+
with pytest.raises(ValueError, match="n_components="):
348347
kpcovc = kpcovc_model(n_components=-1, svd_solver="auto")
349348
kpcovc.fit(X, Y)
350-
assert str(cm.value).startswith("n_components=")
351-
with pytest.raises(ValueError) as cm:
349+
with pytest.raises(ValueError, match="n_components="):
352350
kpcovc = kpcovc_model(n_components=0, svd_solver="randomized")
353351
kpcovc.fit(X, Y)
354-
assert str(cm.value).startswith("n_components=")
355-
with pytest.raises(ValueError) as cm:
352+
with pytest.raises(ValueError, match="n_components="):
356353
kpcovc = kpcovc_model(n_components=X.shape[0], svd_solver="arpack")
357354
kpcovc.fit(X, Y)
358-
assert str(cm.value).startswith("n_components=")
359355
for svd_solver in ["auto", "full"]:
360-
with pytest.raises(ValueError) as cm:
356+
with pytest.raises(ValueError, match="must be of type int"):
361357
kpcovc = kpcovc_model(n_components=np.pi, svd_solver=svd_solver)
362358
kpcovc.fit(X, Y)
363-
assert "must be of type int" in str(cm.value)

0 commit comments

Comments
 (0)