Skip to content

Commit 698419f

Browse files
committed
Annotate the test_choices module
This required a bit of refactoring to get the type of `STATUS` correct for each suite. There are two cases which I decided not to support in the type system: - passing a list instead of a tuple when defining an option group - `in` checks using a data type that doesn't match the choices
1 parent 5f8d45b commit 698419f

File tree

1 file changed

+78
-55
lines changed

1 file changed

+78
-55
lines changed

tests/test_choices.py

Lines changed: 78 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,88 @@
11
from __future__ import annotations
22

3+
from typing import TYPE_CHECKING, Generic, TypeVar
4+
5+
import pytest
36
from django.test import TestCase
47

58
from model_utils import Choices
69

10+
T = TypeVar("T")
711

8-
class ChoicesTests(TestCase):
9-
def setUp(self) -> None:
10-
self.STATUS = Choices('DRAFT', 'PUBLISHED')
1112

12-
def test_getattr(self) -> None:
13-
self.assertEqual(self.STATUS.DRAFT, 'DRAFT')
13+
class ChoicesTestsMixin(Generic[T]):
1414

15-
def test_indexing(self) -> None:
16-
self.assertEqual(self.STATUS['PUBLISHED'], 'PUBLISHED')
15+
STATUS: Choices[T]
1716

18-
def test_iteration(self) -> None:
19-
self.assertEqual(tuple(self.STATUS),
20-
(('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED')))
21-
22-
def test_reversed(self) -> None:
23-
self.assertEqual(tuple(reversed(self.STATUS)),
24-
(('PUBLISHED', 'PUBLISHED'), ('DRAFT', 'DRAFT')))
17+
def test_getattr(self) -> None:
18+
assert self.STATUS.DRAFT == 'DRAFT'
2519

2620
def test_len(self) -> None:
27-
self.assertEqual(len(self.STATUS), 2)
21+
assert len(self.STATUS) == 2
2822

2923
def test_repr(self) -> None:
30-
self.assertEqual(repr(self.STATUS), "Choices" + repr((
24+
assert repr(self.STATUS) == "Choices" + repr((
3125
('DRAFT', 'DRAFT', 'DRAFT'),
3226
('PUBLISHED', 'PUBLISHED', 'PUBLISHED'),
33-
)))
27+
))
3428

3529
def test_wrong_length_tuple(self) -> None:
36-
with self.assertRaises(ValueError):
37-
Choices(('a',))
38-
39-
def test_contains_value(self) -> None:
40-
self.assertTrue('PUBLISHED' in self.STATUS)
41-
self.assertTrue('DRAFT' in self.STATUS)
42-
43-
def test_doesnt_contain_value(self) -> None:
44-
self.assertFalse('UNPUBLISHED' in self.STATUS)
30+
with pytest.raises(ValueError):
31+
Choices(('a',)) # type: ignore[arg-type]
4532

4633
def test_deepcopy(self) -> None:
4734
import copy
48-
self.assertEqual(list(self.STATUS),
49-
list(copy.deepcopy(self.STATUS)))
35+
assert list(self.STATUS) == list(copy.deepcopy(self.STATUS))
5036

5137
def test_equality(self) -> None:
52-
self.assertEqual(self.STATUS, Choices('DRAFT', 'PUBLISHED'))
38+
assert self.STATUS == Choices('DRAFT', 'PUBLISHED')
5339

5440
def test_inequality(self) -> None:
55-
self.assertNotEqual(self.STATUS, ['DRAFT', 'PUBLISHED'])
56-
self.assertNotEqual(self.STATUS, Choices('DRAFT'))
41+
assert self.STATUS != ['DRAFT', 'PUBLISHED']
42+
assert self.STATUS != Choices('DRAFT')
5743

5844
def test_composability(self) -> None:
59-
self.assertEqual(Choices('DRAFT') + Choices('PUBLISHED'), self.STATUS)
60-
self.assertEqual(Choices('DRAFT') + ('PUBLISHED',), self.STATUS)
61-
self.assertEqual(('DRAFT',) + Choices('PUBLISHED'), self.STATUS)
45+
assert Choices('DRAFT') + Choices('PUBLISHED') == self.STATUS
46+
assert Choices('DRAFT') + ('PUBLISHED',) == self.STATUS
47+
assert ('DRAFT',) + Choices('PUBLISHED') == self.STATUS
6248

6349
def test_option_groups(self) -> None:
64-
c = Choices(('group a', ['one', 'two']), ['group b', ('three',)])
65-
self.assertEqual(
66-
list(c),
67-
[
68-
('group a', [('one', 'one'), ('two', 'two')]),
69-
('group b', [('three', 'three')]),
70-
],
71-
)
50+
# Note: The implementation accepts any kind of sequence, but the type system can only
51+
# track per-index types for tuples.
52+
if TYPE_CHECKING:
53+
c = Choices(('group a', ['one', 'two']), ('group b', ('three',)))
54+
else:
55+
c = Choices(('group a', ['one', 'two']), ['group b', ('three',)])
56+
assert list(c) == [
57+
('group a', [('one', 'one'), ('two', 'two')]),
58+
('group b', [('three', 'three')]),
59+
]
60+
61+
62+
class ChoicesTests(TestCase, ChoicesTestsMixin[str]):
63+
def setUp(self) -> None:
64+
self.STATUS = Choices('DRAFT', 'PUBLISHED')
65+
66+
def test_indexing(self) -> None:
67+
self.assertEqual(self.STATUS['PUBLISHED'], 'PUBLISHED')
68+
69+
def test_iteration(self) -> None:
70+
self.assertEqual(tuple(self.STATUS),
71+
(('DRAFT', 'DRAFT'), ('PUBLISHED', 'PUBLISHED')))
72+
73+
def test_reversed(self) -> None:
74+
self.assertEqual(tuple(reversed(self.STATUS)),
75+
(('PUBLISHED', 'PUBLISHED'), ('DRAFT', 'DRAFT')))
76+
77+
def test_contains_value(self) -> None:
78+
self.assertTrue('PUBLISHED' in self.STATUS)
79+
self.assertTrue('DRAFT' in self.STATUS)
80+
81+
def test_doesnt_contain_value(self) -> None:
82+
self.assertFalse('UNPUBLISHED' in self.STATUS)
7283

7384

74-
class LabelChoicesTests(ChoicesTests):
85+
class LabelChoicesTests(TestCase, ChoicesTestsMixin[str]):
7586
def setUp(self) -> None:
7687
self.STATUS = Choices(
7788
('DRAFT', 'is draft'),
@@ -157,10 +168,16 @@ def test_composability(self) -> None:
157168
)
158169

159170
def test_option_groups(self) -> None:
160-
c = Choices(
161-
('group a', [(1, 'one'), (2, 'two')]),
162-
['group b', ((3, 'three'),)]
163-
)
171+
if TYPE_CHECKING:
172+
c = Choices[int](
173+
('group a', [(1, 'one'), (2, 'two')]),
174+
('group b', ((3, 'three'),))
175+
)
176+
else:
177+
c = Choices(
178+
('group a', [(1, 'one'), (2, 'two')]),
179+
['group b', ((3, 'three'),)]
180+
)
164181
self.assertEqual(
165182
list(c),
166183
[
@@ -170,7 +187,7 @@ def test_option_groups(self) -> None:
170187
)
171188

172189

173-
class IdentifierChoicesTests(ChoicesTests):
190+
class IdentifierChoicesTests(TestCase, ChoicesTestsMixin[int]):
174191
def setUp(self) -> None:
175192
self.STATUS = Choices(
176193
(0, 'DRAFT', 'is draft'),
@@ -216,10 +233,10 @@ def test_doesnt_contain_value(self) -> None:
216233
self.assertFalse(3 in self.STATUS)
217234

218235
def test_doesnt_contain_display_value(self) -> None:
219-
self.assertFalse('is draft' in self.STATUS)
236+
self.assertFalse('is draft' in self.STATUS) # type: ignore[operator]
220237

221238
def test_doesnt_contain_python_attr(self) -> None:
222-
self.assertFalse('PUBLISHED' in self.STATUS)
239+
self.assertFalse('PUBLISHED' in self.STATUS) # type: ignore[operator]
223240

224241
def test_equality(self) -> None:
225242
self.assertEqual(self.STATUS, Choices(
@@ -268,10 +285,16 @@ def test_composability(self) -> None:
268285
)
269286

270287
def test_option_groups(self) -> None:
271-
c = Choices(
272-
('group a', [(1, 'ONE', 'one'), (2, 'TWO', 'two')]),
273-
['group b', ((3, 'THREE', 'three'),)]
274-
)
288+
if TYPE_CHECKING:
289+
c = Choices[int](
290+
('group a', [(1, 'ONE', 'one'), (2, 'TWO', 'two')]),
291+
('group b', ((3, 'THREE', 'three'),))
292+
)
293+
else:
294+
c = Choices(
295+
('group a', [(1, 'ONE', 'one'), (2, 'TWO', 'two')]),
296+
['group b', ((3, 'THREE', 'three'),)]
297+
)
275298
self.assertEqual(
276299
list(c),
277300
[
@@ -284,7 +307,7 @@ def test_option_groups(self) -> None:
284307
class SubsetChoicesTest(TestCase):
285308

286309
def setUp(self) -> None:
287-
self.choices = Choices(
310+
self.choices = Choices[int](
288311
(0, 'a', 'A'),
289312
(1, 'b', 'B'),
290313
)

0 commit comments

Comments
 (0)