unittest-parametrize 1.6.0__tar.gz → 1.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,26 @@
2
2
  Changelog
3
3
  =========
4
4
 
5
+ 1.7.0 (2025-08-27)
6
+ ------------------
7
+
8
+ * Add support for the ``ids`` argument being a callable.
9
+ The callable is called once per parameter value and can return a string for that value or ``None`` to use the default.
10
+
11
+ `PR #143 <https://github.com/adamchainz/unittest-parametrize/pull/143>`__.
12
+
13
+ * Fail for collisions between autogenerated IDs and user-specified IDs when mixing tuples and ``param`` instances.
14
+
15
+ `PR #143 <https://github.com/adamchainz/unittest-parametrize/pull/143>`__.
16
+
17
+ * Update the type hints to allow mixing tuples and ``param`` instances.
18
+
19
+ `PR #143 <https://github.com/adamchainz/unittest-parametrize/pull/143>`__.
20
+
5
21
  1.6.0 (2025-01-06)
6
22
  ------------------
7
23
 
8
- * Add suport for asynchronous tests.
24
+ * Add support for asynchronous tests.
9
25
 
10
26
  Thanks to Adrien Cossa in `PR #121 <https://github.com/adamchainz/unittest-parametrize/pull/121>`__.
11
27
 
@@ -1,15 +1,15 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: unittest-parametrize
3
- Version: 1.6.0
3
+ Version: 1.7.0
4
4
  Summary: Parametrize tests within unittest TestCases.
5
5
  Author-email: Adam Johnson <me@adamj.eu>
6
+ License-Expression: MIT
6
7
  Project-URL: Changelog, https://github.com/adamchainz/unittest-parametrize/blob/main/CHANGELOG.rst
7
8
  Project-URL: Funding, https://adamj.eu/books/
8
9
  Project-URL: Repository, https://github.com/adamchainz/unittest-parametrize
9
10
  Keywords: unittest
10
11
  Classifier: Development Status :: 5 - Production/Stable
11
12
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Natural Language :: English
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Classifier: Programming Language :: Python :: 3.9
@@ -22,6 +22,7 @@ Requires-Python: >=3.9
22
22
  Description-Content-Type: text/x-rst
23
23
  License-File: LICENSE
24
24
  Requires-Dist: typing-extensions; python_version < "3.10"
25
+ Dynamic: license-file
25
26
 
26
27
  ====================
27
28
  unittest-parametrize
@@ -83,8 +84,7 @@ Here’s a basic example:
83
84
 
84
85
  .. code-block:: python
85
86
 
86
- from unittest_parametrize import parametrize
87
- from unittest_parametrize import ParametrizedTestCase
87
+ from unittest_parametrize import ParametrizedTestCase, parametrize
88
88
 
89
89
 
90
90
  class SquareTests(ParametrizedTestCase):
@@ -113,8 +113,7 @@ You can provide argument names as a sequence of strings instead:
113
113
 
114
114
  .. code-block:: python
115
115
 
116
- from unittest_parametrize import parametrize
117
- from unittest_parametrize import ParametrizedTestCase
116
+ from unittest_parametrize import ParametrizedTestCase, parametrize
118
117
 
119
118
 
120
119
  class SquareTests(ParametrizedTestCase):
@@ -177,13 +176,20 @@ You can see these names when running the tests:
177
176
 
178
177
  OK
179
178
 
180
- You can customize these names by passing ``param`` objects, which contain the arguments and an optional ID for the suffix:
179
+ You can customize these names in several ways:
180
+
181
+ 1. Using ``param`` objects with IDs.
182
+ 2. Passing a sequence of strings as the ``ids`` argument.
183
+ 3. Passing a callable as the ``ids`` argument.
184
+
185
+ Passing ``param`` objects with IDs
186
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
187
+
188
+ Pass a ``param`` object for each parameter set, setting the test ID suffix with the optional ``id`` argument:
181
189
 
182
190
  .. code-block:: python
183
191
 
184
- from unittest_parametrize import param
185
- from unittest_parametrize import parametrize
186
- from unittest_parametrize import ParametrizedTestCase
192
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
187
193
 
188
194
 
189
195
  class SquareTests(ParametrizedTestCase):
@@ -197,7 +203,7 @@ You can customize these names by passing ``param`` objects, which contain the ar
197
203
  def test_square(self, x: int, expected: int) -> None:
198
204
  self.assertEqual(x**2, expected)
199
205
 
200
- Yielding perhaps more natural names:
206
+ Yielding more natural names:
201
207
 
202
208
  .. code-block:: console
203
209
 
@@ -216,9 +222,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
216
222
 
217
223
  .. code-block:: python
218
224
 
219
- from unittest_parametrize import param
220
- from unittest_parametrize import parametrize
221
- from unittest_parametrize import ParametrizedTestCase
225
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
222
226
 
223
227
 
224
228
  class SquareTests(ParametrizedTestCase):
@@ -232,7 +236,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
232
236
  def test_square(self, x: int, expected: int) -> None:
233
237
  self.assertEqual(x**2, expected)
234
238
 
235
- ID-free ``param``\s fall back to the default index suffixes:
239
+ The ID-free ``param``\s fall back to the default index suffixes:
236
240
 
237
241
  .. code-block:: console
238
242
 
@@ -245,12 +249,14 @@ ID-free ``param``\s fall back to the default index suffixes:
245
249
 
246
250
  OK
247
251
 
248
- Alternatively, you can provide the id’s separately with the ``ids`` argument:
252
+ Passing a sequence of strings as the ``ids`` argument
253
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
254
+
255
+ Another option is to provide the IDs in the separate ``ids`` argument:
249
256
 
250
257
  .. code-block:: python
251
258
 
252
- from unittest_parametrize import parametrize
253
- from unittest_parametrize import ParametrizedTestCase
259
+ from unittest_parametrize import ParametrizedTestCase, parametrize
254
260
 
255
261
 
256
262
  class SquareTests(ParametrizedTestCase):
@@ -265,6 +271,64 @@ Alternatively, you can provide the id’s separately with the ``ids`` argument:
265
271
  def test_square(self, x: int, expected: int) -> None:
266
272
  self.assertEqual(x**2, expected)
267
273
 
274
+ This option sets the full suffixes to the provided strings:
275
+
276
+ .. code-block:: console
277
+
278
+ $ python -m unittest t.py -v
279
+ test_square_one (example.SquareTests.test_square_one) ... ok
280
+ test_square_two (example.SquareTests.test_square_two) ... ok
281
+
282
+ ----------------------------------------------------------------------
283
+ Ran 2 tests in 0.000s
284
+
285
+ OK
286
+
287
+ Passing a callable as the ``ids`` argument
288
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
289
+
290
+ The ``ids`` argument can also be a callable, which unittest-parametrize calls once per parameter value.
291
+ The callable can return a string for that value, or ``None`` to use the default index suffix.
292
+ The values are then joined with underscores to form the full suffix.
293
+
294
+ For example:
295
+
296
+ .. code-block:: python
297
+
298
+ from unittest_parametrize import ParametrizedTestCase, parametrize
299
+
300
+
301
+ def make_id(value):
302
+ if isinstance(value, int):
303
+ return f"num{value}"
304
+ return None
305
+
306
+
307
+ class SquareTests(ParametrizedTestCase):
308
+ @parametrize(
309
+ "x,expected",
310
+ [
311
+ (1, 1),
312
+ (2, 4),
313
+ ],
314
+ ids=make_id,
315
+ )
316
+ def test_square(self, x: int, expected: int) -> None:
317
+ self.assertEqual(x**2, expected)
318
+
319
+ …yields:
320
+
321
+ .. code-block:: console
322
+
323
+ $ python -m unittest t.py -v
324
+ test_square_num1_num1 (example.SquareTests.test_square_num1_num1) ... ok
325
+ test_square_num2_num4 (example.SquareTests.test_square_num2_num4) ... ok
326
+
327
+ ----------------------------------------------------------------------
328
+ Ran 2 tests in 0.000s
329
+
330
+ OK
331
+
268
332
  Use with other test decorators
269
333
  ------------------------------
270
334
 
@@ -275,8 +339,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
275
339
  .. code-block:: python
276
340
 
277
341
  from unittest import mock
278
- from unittest_parametrize import parametrize
279
- from unittest_parametrize import ParametrizedTestCase
342
+ from unittest_parametrize import ParametrizedTestCase, parametrize
280
343
 
281
344
 
282
345
  class CarpentryTests(ParametrizedTestCase):
@@ -285,8 +348,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
285
348
  [(11,), (17,)],
286
349
  )
287
350
  @mock.patch("example.hammer", autospec=True)
288
- def test_nail_a_board(self, mock_hammer, nails):
289
- ...
351
+ def test_nail_a_board(self, mock_hammer, nails): ...
290
352
 
291
353
  Also note that due to how ``mock.patch`` always adds positional arguments at the start, the parametrized arguments must come last.
292
354
  ``@parametrize`` always adds parameters as keyword arguments, so you can also use `keyword-only syntax <https://peps.python.org/pep-3102/>`__ for parametrized arguments:
@@ -294,8 +356,7 @@ Also note that due to how ``mock.patch`` always adds positional arguments at the
294
356
  .. code-block:: python
295
357
 
296
358
  # ...
297
- def test_nail_a_board(self, mock_hammer, *, nails):
298
- ...
359
+ def test_nail_a_board(self, mock_hammer, *, nails): ...
299
360
 
300
361
  Multiple ``@parametrize`` decorators
301
362
  ------------------------------------
@@ -305,8 +366,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
305
366
 
306
367
  .. code-block:: python
307
368
 
308
- from unittest_parametrize import parametrize
309
- from unittest_parametrize import ParametrizedTestCase
369
+ from unittest_parametrize import ParametrizedTestCase, parametrize
310
370
 
311
371
 
312
372
  class RocketTests(ParametrizedTestCase):
@@ -318,8 +378,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
318
378
  for hyperdrive_level in [0, 1, 2]
319
379
  ],
320
380
  )
321
- def test_takeoff(self, use_ions, hyperdrive_level) -> None:
322
- ...
381
+ def test_takeoff(self, use_ions, hyperdrive_level) -> None: ...
323
382
 
324
383
  The above creates 2 * 3 = 6 versions of ``test_takeoff``.
325
384
 
@@ -331,8 +390,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
331
390
  .. code-block:: python
332
391
 
333
392
  from itertools import product
334
- from unittest_parametrize import parametrize
335
- from unittest_parametrize import ParametrizedTestCase
393
+ from unittest_parametrize import ParametrizedTestCase, parametrize
336
394
 
337
395
 
338
396
  class RocketTests(ParametrizedTestCase):
@@ -346,8 +404,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
346
404
  )
347
405
  ),
348
406
  )
349
- def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None:
350
- ...
407
+ def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None: ...
351
408
 
352
409
  The above creates 2 * 3 * 2 = 12 versions of ``test_takeoff``.
353
410
 
@@ -371,15 +428,47 @@ To parametrize all tests within a test case, create a separate decorator and app
371
428
 
372
429
  class StatsTests(ParametrizedTestCase):
373
430
  @parametrize_race
374
- def test_strength(self, race: str) -> None:
375
- ...
431
+ def test_strength(self, race: str) -> None: ...
376
432
 
377
433
  @parametrize_race
378
- def test_dexterity(self, race: str) -> None:
379
- ...
434
+ def test_dexterity(self, race: str) -> None: ...
380
435
 
381
436
  ...
382
437
 
438
+ Pass parameters in a dataclass
439
+ ------------------------------
440
+
441
+ Thanks to `Florian Bruhin <https://bruhin.software/>`__ for this tip, from his `pytest tips and tricks presentation <https://bruhin.software/>`__.
442
+
443
+ If your test uses many parameters or cases, the parametrization may become unwieldy, as cases don’t name the arguments.
444
+ In this case, try using a `dataclass <https://docs.python.org/3/library/dataclasses.html>`__ to hold the arguments:
445
+
446
+ .. code-block:: python
447
+
448
+ from dataclasses import dataclass
449
+
450
+ from unittest_parametrize import ParametrizedTestCase, parametrize
451
+
452
+
453
+ @dataclass
454
+ class SquareParams:
455
+ x: int
456
+ expected: int
457
+
458
+
459
+ class SquareTests(ParametrizedTestCase):
460
+ @parametrize(
461
+ "sp",
462
+ [
463
+ (SquareParams(x=1, expected=1),),
464
+ (SquareParams(x=2, expected=4),),
465
+ ],
466
+ )
467
+ def test_square(self, sp: SquareParams) -> None:
468
+ self.assertEqual(sp.x**2, sp.expected)
469
+
470
+ This way, each parameter is type-checked and named, improving safety and readability.
471
+
383
472
  History
384
473
  =======
385
474
 
@@ -58,8 +58,7 @@ Here’s a basic example:
58
58
 
59
59
  .. code-block:: python
60
60
 
61
- from unittest_parametrize import parametrize
62
- from unittest_parametrize import ParametrizedTestCase
61
+ from unittest_parametrize import ParametrizedTestCase, parametrize
63
62
 
64
63
 
65
64
  class SquareTests(ParametrizedTestCase):
@@ -88,8 +87,7 @@ You can provide argument names as a sequence of strings instead:
88
87
 
89
88
  .. code-block:: python
90
89
 
91
- from unittest_parametrize import parametrize
92
- from unittest_parametrize import ParametrizedTestCase
90
+ from unittest_parametrize import ParametrizedTestCase, parametrize
93
91
 
94
92
 
95
93
  class SquareTests(ParametrizedTestCase):
@@ -152,13 +150,20 @@ You can see these names when running the tests:
152
150
 
153
151
  OK
154
152
 
155
- You can customize these names by passing ``param`` objects, which contain the arguments and an optional ID for the suffix:
153
+ You can customize these names in several ways:
154
+
155
+ 1. Using ``param`` objects with IDs.
156
+ 2. Passing a sequence of strings as the ``ids`` argument.
157
+ 3. Passing a callable as the ``ids`` argument.
158
+
159
+ Passing ``param`` objects with IDs
160
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
161
+
162
+ Pass a ``param`` object for each parameter set, setting the test ID suffix with the optional ``id`` argument:
156
163
 
157
164
  .. code-block:: python
158
165
 
159
- from unittest_parametrize import param
160
- from unittest_parametrize import parametrize
161
- from unittest_parametrize import ParametrizedTestCase
166
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
162
167
 
163
168
 
164
169
  class SquareTests(ParametrizedTestCase):
@@ -172,7 +177,7 @@ You can customize these names by passing ``param`` objects, which contain the ar
172
177
  def test_square(self, x: int, expected: int) -> None:
173
178
  self.assertEqual(x**2, expected)
174
179
 
175
- Yielding perhaps more natural names:
180
+ Yielding more natural names:
176
181
 
177
182
  .. code-block:: console
178
183
 
@@ -191,9 +196,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
191
196
 
192
197
  .. code-block:: python
193
198
 
194
- from unittest_parametrize import param
195
- from unittest_parametrize import parametrize
196
- from unittest_parametrize import ParametrizedTestCase
199
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
197
200
 
198
201
 
199
202
  class SquareTests(ParametrizedTestCase):
@@ -207,7 +210,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
207
210
  def test_square(self, x: int, expected: int) -> None:
208
211
  self.assertEqual(x**2, expected)
209
212
 
210
- ID-free ``param``\s fall back to the default index suffixes:
213
+ The ID-free ``param``\s fall back to the default index suffixes:
211
214
 
212
215
  .. code-block:: console
213
216
 
@@ -220,12 +223,14 @@ ID-free ``param``\s fall back to the default index suffixes:
220
223
 
221
224
  OK
222
225
 
223
- Alternatively, you can provide the id’s separately with the ``ids`` argument:
226
+ Passing a sequence of strings as the ``ids`` argument
227
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
228
+
229
+ Another option is to provide the IDs in the separate ``ids`` argument:
224
230
 
225
231
  .. code-block:: python
226
232
 
227
- from unittest_parametrize import parametrize
228
- from unittest_parametrize import ParametrizedTestCase
233
+ from unittest_parametrize import ParametrizedTestCase, parametrize
229
234
 
230
235
 
231
236
  class SquareTests(ParametrizedTestCase):
@@ -240,6 +245,64 @@ Alternatively, you can provide the id’s separately with the ``ids`` argument:
240
245
  def test_square(self, x: int, expected: int) -> None:
241
246
  self.assertEqual(x**2, expected)
242
247
 
248
+ This option sets the full suffixes to the provided strings:
249
+
250
+ .. code-block:: console
251
+
252
+ $ python -m unittest t.py -v
253
+ test_square_one (example.SquareTests.test_square_one) ... ok
254
+ test_square_two (example.SquareTests.test_square_two) ... ok
255
+
256
+ ----------------------------------------------------------------------
257
+ Ran 2 tests in 0.000s
258
+
259
+ OK
260
+
261
+ Passing a callable as the ``ids`` argument
262
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
263
+
264
+ The ``ids`` argument can also be a callable, which unittest-parametrize calls once per parameter value.
265
+ The callable can return a string for that value, or ``None`` to use the default index suffix.
266
+ The values are then joined with underscores to form the full suffix.
267
+
268
+ For example:
269
+
270
+ .. code-block:: python
271
+
272
+ from unittest_parametrize import ParametrizedTestCase, parametrize
273
+
274
+
275
+ def make_id(value):
276
+ if isinstance(value, int):
277
+ return f"num{value}"
278
+ return None
279
+
280
+
281
+ class SquareTests(ParametrizedTestCase):
282
+ @parametrize(
283
+ "x,expected",
284
+ [
285
+ (1, 1),
286
+ (2, 4),
287
+ ],
288
+ ids=make_id,
289
+ )
290
+ def test_square(self, x: int, expected: int) -> None:
291
+ self.assertEqual(x**2, expected)
292
+
293
+ …yields:
294
+
295
+ .. code-block:: console
296
+
297
+ $ python -m unittest t.py -v
298
+ test_square_num1_num1 (example.SquareTests.test_square_num1_num1) ... ok
299
+ test_square_num2_num4 (example.SquareTests.test_square_num2_num4) ... ok
300
+
301
+ ----------------------------------------------------------------------
302
+ Ran 2 tests in 0.000s
303
+
304
+ OK
305
+
243
306
  Use with other test decorators
244
307
  ------------------------------
245
308
 
@@ -250,8 +313,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
250
313
  .. code-block:: python
251
314
 
252
315
  from unittest import mock
253
- from unittest_parametrize import parametrize
254
- from unittest_parametrize import ParametrizedTestCase
316
+ from unittest_parametrize import ParametrizedTestCase, parametrize
255
317
 
256
318
 
257
319
  class CarpentryTests(ParametrizedTestCase):
@@ -260,8 +322,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
260
322
  [(11,), (17,)],
261
323
  )
262
324
  @mock.patch("example.hammer", autospec=True)
263
- def test_nail_a_board(self, mock_hammer, nails):
264
- ...
325
+ def test_nail_a_board(self, mock_hammer, nails): ...
265
326
 
266
327
  Also note that due to how ``mock.patch`` always adds positional arguments at the start, the parametrized arguments must come last.
267
328
  ``@parametrize`` always adds parameters as keyword arguments, so you can also use `keyword-only syntax <https://peps.python.org/pep-3102/>`__ for parametrized arguments:
@@ -269,8 +330,7 @@ Also note that due to how ``mock.patch`` always adds positional arguments at the
269
330
  .. code-block:: python
270
331
 
271
332
  # ...
272
- def test_nail_a_board(self, mock_hammer, *, nails):
273
- ...
333
+ def test_nail_a_board(self, mock_hammer, *, nails): ...
274
334
 
275
335
  Multiple ``@parametrize`` decorators
276
336
  ------------------------------------
@@ -280,8 +340,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
280
340
 
281
341
  .. code-block:: python
282
342
 
283
- from unittest_parametrize import parametrize
284
- from unittest_parametrize import ParametrizedTestCase
343
+ from unittest_parametrize import ParametrizedTestCase, parametrize
285
344
 
286
345
 
287
346
  class RocketTests(ParametrizedTestCase):
@@ -293,8 +352,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
293
352
  for hyperdrive_level in [0, 1, 2]
294
353
  ],
295
354
  )
296
- def test_takeoff(self, use_ions, hyperdrive_level) -> None:
297
- ...
355
+ def test_takeoff(self, use_ions, hyperdrive_level) -> None: ...
298
356
 
299
357
  The above creates 2 * 3 = 6 versions of ``test_takeoff``.
300
358
 
@@ -306,8 +364,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
306
364
  .. code-block:: python
307
365
 
308
366
  from itertools import product
309
- from unittest_parametrize import parametrize
310
- from unittest_parametrize import ParametrizedTestCase
367
+ from unittest_parametrize import ParametrizedTestCase, parametrize
311
368
 
312
369
 
313
370
  class RocketTests(ParametrizedTestCase):
@@ -321,8 +378,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
321
378
  )
322
379
  ),
323
380
  )
324
- def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None:
325
- ...
381
+ def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None: ...
326
382
 
327
383
  The above creates 2 * 3 * 2 = 12 versions of ``test_takeoff``.
328
384
 
@@ -346,15 +402,47 @@ To parametrize all tests within a test case, create a separate decorator and app
346
402
 
347
403
  class StatsTests(ParametrizedTestCase):
348
404
  @parametrize_race
349
- def test_strength(self, race: str) -> None:
350
- ...
405
+ def test_strength(self, race: str) -> None: ...
351
406
 
352
407
  @parametrize_race
353
- def test_dexterity(self, race: str) -> None:
354
- ...
408
+ def test_dexterity(self, race: str) -> None: ...
355
409
 
356
410
  ...
357
411
 
412
+ Pass parameters in a dataclass
413
+ ------------------------------
414
+
415
+ Thanks to `Florian Bruhin <https://bruhin.software/>`__ for this tip, from his `pytest tips and tricks presentation <https://bruhin.software/>`__.
416
+
417
+ If your test uses many parameters or cases, the parametrization may become unwieldy, as cases don’t name the arguments.
418
+ In this case, try using a `dataclass <https://docs.python.org/3/library/dataclasses.html>`__ to hold the arguments:
419
+
420
+ .. code-block:: python
421
+
422
+ from dataclasses import dataclass
423
+
424
+ from unittest_parametrize import ParametrizedTestCase, parametrize
425
+
426
+
427
+ @dataclass
428
+ class SquareParams:
429
+ x: int
430
+ expected: int
431
+
432
+
433
+ class SquareTests(ParametrizedTestCase):
434
+ @parametrize(
435
+ "sp",
436
+ [
437
+ (SquareParams(x=1, expected=1),),
438
+ (SquareParams(x=2, expected=4),),
439
+ ],
440
+ )
441
+ def test_square(self, sp: SquareParams) -> None:
442
+ self.assertEqual(sp.x**2, sp.expected)
443
+
444
+ This way, each parameter is type-checked and named, improving safety and readability.
445
+
358
446
  History
359
447
  =======
360
448
 
@@ -1,17 +1,19 @@
1
1
  [build-system]
2
2
  build-backend = "setuptools.build_meta"
3
3
  requires = [
4
- "setuptools",
4
+ "setuptools>=77",
5
5
  ]
6
6
 
7
7
  [project]
8
8
  name = "unittest-parametrize"
9
- version = "1.6.0"
9
+ version = "1.7.0"
10
10
  description = "Parametrize tests within unittest TestCases."
11
11
  readme = "README.rst"
12
12
  keywords = [
13
13
  "unittest",
14
14
  ]
15
+ license = "MIT"
16
+ license-files = [ "LICENSE" ]
15
17
  authors = [
16
18
  { name = "Adam Johnson", email = "me@adamj.eu" },
17
19
  ]
@@ -19,7 +21,6 @@ requires-python = ">=3.9"
19
21
  classifiers = [
20
22
  "Development Status :: 5 - Production/Stable",
21
23
  "Intended Audience :: Developers",
22
- "License :: OSI Approved :: MIT License",
23
24
  "Natural Language :: English",
24
25
  "Programming Language :: Python :: 3 :: Only",
25
26
  "Programming Language :: Python :: 3.9",
@@ -36,12 +37,50 @@ urls.Changelog = "https://github.com/adamchainz/unittest-parametrize/blob/main/C
36
37
  urls.Funding = "https://adamj.eu/books/"
37
38
  urls.Repository = "https://github.com/adamchainz/unittest-parametrize"
38
39
 
39
- [tool.isort]
40
- add_imports = [
41
- "from __future__ import annotations",
40
+ [dependency-groups]
41
+ test = [
42
+ "coverage[toml]",
43
+ "pytest",
44
+ "pytest-randomly",
45
+ "typing-extensions; python_version<'3.10'",
46
+ ]
47
+
48
+ [tool.ruff]
49
+ lint.select = [
50
+ # flake8-bugbear
51
+ "B",
52
+ # flake8-comprehensions
53
+ "C4",
54
+ # pycodestyle
55
+ "E",
56
+ # Pyflakes errors
57
+ "F",
58
+ # isort
59
+ "I",
60
+ # flake8-simplify
61
+ "SIM",
62
+ # flake8-tidy-imports
63
+ "TID",
64
+ # pyupgrade
65
+ "UP",
66
+ # Pyflakes warnings
67
+ "W",
68
+ ]
69
+ lint.ignore = [
70
+ # flake8-bugbear opinionated rules
71
+ "B9",
72
+ # line-too-long
73
+ "E501",
74
+ # suppressible-exception
75
+ "SIM105",
76
+ # if-else-block-instead-of-if-exp
77
+ "SIM108",
78
+ ]
79
+ lint.extend-safe-fixes = [
80
+ # non-pep585-annotation
81
+ "UP006",
42
82
  ]
43
- force_single_line = true
44
- profile = "black"
83
+ lint.isort.required-imports = [ "from __future__ import annotations" ]
45
84
 
46
85
  [tool.pyproject-fmt]
47
86
  max_supported_python = "3.13"
@@ -5,9 +5,7 @@ import sys
5
5
  from collections.abc import Sequence
6
6
  from functools import wraps
7
7
  from types import FunctionType
8
- from typing import Any
9
- from typing import Callable
10
- from typing import TypeVar
8
+ from typing import Any, Callable, TypeVar
11
9
  from unittest import TestCase
12
10
 
13
11
  if sys.version_info >= (3, 10):
@@ -122,8 +120,8 @@ TestFunc = Callable[P, T]
122
120
 
123
121
  def parametrize(
124
122
  argnames: str | Sequence[str],
125
- argvalues: Sequence[tuple[Any, ...]] | Sequence[param],
126
- ids: Sequence[str | None] | None = None,
123
+ argvalues: Sequence[tuple[Any, ...] | param],
124
+ ids: Sequence[str | None] | Callable[[Any], str | None] | None = None,
127
125
  ) -> Callable[[Callable[P, T]], Callable[P, T]]:
128
126
  if isinstance(argnames, str):
129
127
  argnames = [a.strip() for a in argnames.split(",")]
@@ -131,24 +129,22 @@ def parametrize(
131
129
  if len(argnames) == 0:
132
130
  raise ValueError("argnames must contain at least one element")
133
131
 
134
- if ids is not None and len(ids) != len(argvalues):
132
+ ids_callable = callable(ids)
133
+ if ids is not None and not ids_callable and len(ids) != len(argvalues): # type: ignore[arg-type]
135
134
  raise ValueError("ids must have the same length as argvalues")
136
135
 
137
136
  seen_ids = set()
138
137
  params = []
139
138
  for i, argvalue in enumerate(argvalues):
140
- if ids and ids[i]:
141
- id_ = ids[i]
142
- else:
143
- id_ = str(i)
144
-
145
139
  if isinstance(argvalue, tuple):
146
140
  if len(argvalue) != len(argnames):
147
141
  raise ValueError(
148
142
  f"tuple at index {i} has wrong number of arguments "
149
143
  + f"({len(argvalue)} != {len(argnames)})"
150
144
  )
151
- params.append(param(*argvalue, id=id_))
145
+ argvalue = param(*argvalue, id=make_id(i, argvalue, ids))
146
+ params.append(argvalue)
147
+ seen_ids.add(argvalue.id)
152
148
  elif isinstance(argvalue, param):
153
149
  if len(argvalue.args) != len(argnames):
154
150
  raise ValueError(
@@ -157,7 +153,7 @@ def parametrize(
157
153
  )
158
154
 
159
155
  if argvalue.id is None:
160
- argvalue = param(*argvalue.args, id=id_)
156
+ argvalue = param(*argvalue.args, id=make_id(i, argvalue, ids))
161
157
  if argvalue.id in seen_ids:
162
158
  raise ValueError(f"Duplicate param id {argvalue.id!r}")
163
159
  seen_ids.add(argvalue.id)
@@ -183,3 +179,34 @@ def parametrize(
183
179
  return func
184
180
 
185
181
  return wrapper
182
+
183
+
184
+ def make_id(
185
+ i: int,
186
+ argvalue: tuple[Any, ...] | param,
187
+ ids: Sequence[str | None] | Callable[[Any], str | None] | None,
188
+ ) -> str:
189
+ if callable(ids):
190
+ if isinstance(argvalue, tuple):
191
+ values = argvalue
192
+ else:
193
+ values = argvalue.args
194
+
195
+ id_parts = []
196
+ for value in values:
197
+ id_part = ids(value)
198
+ if id_part is not None:
199
+ id_parts.append(id_part)
200
+ else:
201
+ id_parts.append(str(value))
202
+ id_ = "_".join(id_parts)
203
+ # Validate the generated ID
204
+ if not f"_{id_}".isidentifier():
205
+ raise ValueError(
206
+ f"callable ids returned invalid Python identifier suffix: {id_!r}"
207
+ )
208
+ return id_
209
+ elif ids and ids[i]:
210
+ return str(ids[i])
211
+ else:
212
+ return str(i)
@@ -1,15 +1,15 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: unittest-parametrize
3
- Version: 1.6.0
3
+ Version: 1.7.0
4
4
  Summary: Parametrize tests within unittest TestCases.
5
5
  Author-email: Adam Johnson <me@adamj.eu>
6
+ License-Expression: MIT
6
7
  Project-URL: Changelog, https://github.com/adamchainz/unittest-parametrize/blob/main/CHANGELOG.rst
7
8
  Project-URL: Funding, https://adamj.eu/books/
8
9
  Project-URL: Repository, https://github.com/adamchainz/unittest-parametrize
9
10
  Keywords: unittest
10
11
  Classifier: Development Status :: 5 - Production/Stable
11
12
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Natural Language :: English
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Classifier: Programming Language :: Python :: 3.9
@@ -22,6 +22,7 @@ Requires-Python: >=3.9
22
22
  Description-Content-Type: text/x-rst
23
23
  License-File: LICENSE
24
24
  Requires-Dist: typing-extensions; python_version < "3.10"
25
+ Dynamic: license-file
25
26
 
26
27
  ====================
27
28
  unittest-parametrize
@@ -83,8 +84,7 @@ Here’s a basic example:
83
84
 
84
85
  .. code-block:: python
85
86
 
86
- from unittest_parametrize import parametrize
87
- from unittest_parametrize import ParametrizedTestCase
87
+ from unittest_parametrize import ParametrizedTestCase, parametrize
88
88
 
89
89
 
90
90
  class SquareTests(ParametrizedTestCase):
@@ -113,8 +113,7 @@ You can provide argument names as a sequence of strings instead:
113
113
 
114
114
  .. code-block:: python
115
115
 
116
- from unittest_parametrize import parametrize
117
- from unittest_parametrize import ParametrizedTestCase
116
+ from unittest_parametrize import ParametrizedTestCase, parametrize
118
117
 
119
118
 
120
119
  class SquareTests(ParametrizedTestCase):
@@ -177,13 +176,20 @@ You can see these names when running the tests:
177
176
 
178
177
  OK
179
178
 
180
- You can customize these names by passing ``param`` objects, which contain the arguments and an optional ID for the suffix:
179
+ You can customize these names in several ways:
180
+
181
+ 1. Using ``param`` objects with IDs.
182
+ 2. Passing a sequence of strings as the ``ids`` argument.
183
+ 3. Passing a callable as the ``ids`` argument.
184
+
185
+ Passing ``param`` objects with IDs
186
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
187
+
188
+ Pass a ``param`` object for each parameter set, setting the test ID suffix with the optional ``id`` argument:
181
189
 
182
190
  .. code-block:: python
183
191
 
184
- from unittest_parametrize import param
185
- from unittest_parametrize import parametrize
186
- from unittest_parametrize import ParametrizedTestCase
192
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
187
193
 
188
194
 
189
195
  class SquareTests(ParametrizedTestCase):
@@ -197,7 +203,7 @@ You can customize these names by passing ``param`` objects, which contain the ar
197
203
  def test_square(self, x: int, expected: int) -> None:
198
204
  self.assertEqual(x**2, expected)
199
205
 
200
- Yielding perhaps more natural names:
206
+ Yielding more natural names:
201
207
 
202
208
  .. code-block:: console
203
209
 
@@ -216,9 +222,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
216
222
 
217
223
  .. code-block:: python
218
224
 
219
- from unittest_parametrize import param
220
- from unittest_parametrize import parametrize
221
- from unittest_parametrize import ParametrizedTestCase
225
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
222
226
 
223
227
 
224
228
  class SquareTests(ParametrizedTestCase):
@@ -232,7 +236,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
232
236
  def test_square(self, x: int, expected: int) -> None:
233
237
  self.assertEqual(x**2, expected)
234
238
 
235
- ID-free ``param``\s fall back to the default index suffixes:
239
+ The ID-free ``param``\s fall back to the default index suffixes:
236
240
 
237
241
  .. code-block:: console
238
242
 
@@ -245,12 +249,14 @@ ID-free ``param``\s fall back to the default index suffixes:
245
249
 
246
250
  OK
247
251
 
248
- Alternatively, you can provide the id’s separately with the ``ids`` argument:
252
+ Passing a sequence of strings as the ``ids`` argument
253
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
254
+
255
+ Another option is to provide the IDs in the separate ``ids`` argument:
249
256
 
250
257
  .. code-block:: python
251
258
 
252
- from unittest_parametrize import parametrize
253
- from unittest_parametrize import ParametrizedTestCase
259
+ from unittest_parametrize import ParametrizedTestCase, parametrize
254
260
 
255
261
 
256
262
  class SquareTests(ParametrizedTestCase):
@@ -265,6 +271,64 @@ Alternatively, you can provide the id’s separately with the ``ids`` argument:
265
271
  def test_square(self, x: int, expected: int) -> None:
266
272
  self.assertEqual(x**2, expected)
267
273
 
274
+ This option sets the full suffixes to the provided strings:
275
+
276
+ .. code-block:: console
277
+
278
+ $ python -m unittest t.py -v
279
+ test_square_one (example.SquareTests.test_square_one) ... ok
280
+ test_square_two (example.SquareTests.test_square_two) ... ok
281
+
282
+ ----------------------------------------------------------------------
283
+ Ran 2 tests in 0.000s
284
+
285
+ OK
286
+
287
+ Passing a callable as the ``ids`` argument
288
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
289
+
290
+ The ``ids`` argument can also be a callable, which unittest-parametrize calls once per parameter value.
291
+ The callable can return a string for that value, or ``None`` to use the default index suffix.
292
+ The values are then joined with underscores to form the full suffix.
293
+
294
+ For example:
295
+
296
+ .. code-block:: python
297
+
298
+ from unittest_parametrize import ParametrizedTestCase, parametrize
299
+
300
+
301
+ def make_id(value):
302
+ if isinstance(value, int):
303
+ return f"num{value}"
304
+ return None
305
+
306
+
307
+ class SquareTests(ParametrizedTestCase):
308
+ @parametrize(
309
+ "x,expected",
310
+ [
311
+ (1, 1),
312
+ (2, 4),
313
+ ],
314
+ ids=make_id,
315
+ )
316
+ def test_square(self, x: int, expected: int) -> None:
317
+ self.assertEqual(x**2, expected)
318
+
319
+ …yields:
320
+
321
+ .. code-block:: console
322
+
323
+ $ python -m unittest t.py -v
324
+ test_square_num1_num1 (example.SquareTests.test_square_num1_num1) ... ok
325
+ test_square_num2_num4 (example.SquareTests.test_square_num2_num4) ... ok
326
+
327
+ ----------------------------------------------------------------------
328
+ Ran 2 tests in 0.000s
329
+
330
+ OK
331
+
268
332
  Use with other test decorators
269
333
  ------------------------------
270
334
 
@@ -275,8 +339,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
275
339
  .. code-block:: python
276
340
 
277
341
  from unittest import mock
278
- from unittest_parametrize import parametrize
279
- from unittest_parametrize import ParametrizedTestCase
342
+ from unittest_parametrize import ParametrizedTestCase, parametrize
280
343
 
281
344
 
282
345
  class CarpentryTests(ParametrizedTestCase):
@@ -285,8 +348,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
285
348
  [(11,), (17,)],
286
349
  )
287
350
  @mock.patch("example.hammer", autospec=True)
288
- def test_nail_a_board(self, mock_hammer, nails):
289
- ...
351
+ def test_nail_a_board(self, mock_hammer, nails): ...
290
352
 
291
353
  Also note that due to how ``mock.patch`` always adds positional arguments at the start, the parametrized arguments must come last.
292
354
  ``@parametrize`` always adds parameters as keyword arguments, so you can also use `keyword-only syntax <https://peps.python.org/pep-3102/>`__ for parametrized arguments:
@@ -294,8 +356,7 @@ Also note that due to how ``mock.patch`` always adds positional arguments at the
294
356
  .. code-block:: python
295
357
 
296
358
  # ...
297
- def test_nail_a_board(self, mock_hammer, *, nails):
298
- ...
359
+ def test_nail_a_board(self, mock_hammer, *, nails): ...
299
360
 
300
361
  Multiple ``@parametrize`` decorators
301
362
  ------------------------------------
@@ -305,8 +366,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
305
366
 
306
367
  .. code-block:: python
307
368
 
308
- from unittest_parametrize import parametrize
309
- from unittest_parametrize import ParametrizedTestCase
369
+ from unittest_parametrize import ParametrizedTestCase, parametrize
310
370
 
311
371
 
312
372
  class RocketTests(ParametrizedTestCase):
@@ -318,8 +378,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
318
378
  for hyperdrive_level in [0, 1, 2]
319
379
  ],
320
380
  )
321
- def test_takeoff(self, use_ions, hyperdrive_level) -> None:
322
- ...
381
+ def test_takeoff(self, use_ions, hyperdrive_level) -> None: ...
323
382
 
324
383
  The above creates 2 * 3 = 6 versions of ``test_takeoff``.
325
384
 
@@ -331,8 +390,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
331
390
  .. code-block:: python
332
391
 
333
392
  from itertools import product
334
- from unittest_parametrize import parametrize
335
- from unittest_parametrize import ParametrizedTestCase
393
+ from unittest_parametrize import ParametrizedTestCase, parametrize
336
394
 
337
395
 
338
396
  class RocketTests(ParametrizedTestCase):
@@ -346,8 +404,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
346
404
  )
347
405
  ),
348
406
  )
349
- def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None:
350
- ...
407
+ def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None: ...
351
408
 
352
409
  The above creates 2 * 3 * 2 = 12 versions of ``test_takeoff``.
353
410
 
@@ -371,15 +428,47 @@ To parametrize all tests within a test case, create a separate decorator and app
371
428
 
372
429
  class StatsTests(ParametrizedTestCase):
373
430
  @parametrize_race
374
- def test_strength(self, race: str) -> None:
375
- ...
431
+ def test_strength(self, race: str) -> None: ...
376
432
 
377
433
  @parametrize_race
378
- def test_dexterity(self, race: str) -> None:
379
- ...
434
+ def test_dexterity(self, race: str) -> None: ...
380
435
 
381
436
  ...
382
437
 
438
+ Pass parameters in a dataclass
439
+ ------------------------------
440
+
441
+ Thanks to `Florian Bruhin <https://bruhin.software/>`__ for this tip, from his `pytest tips and tricks presentation <https://bruhin.software/>`__.
442
+
443
+ If your test uses many parameters or cases, the parametrization may become unwieldy, as cases don’t name the arguments.
444
+ In this case, try using a `dataclass <https://docs.python.org/3/library/dataclasses.html>`__ to hold the arguments:
445
+
446
+ .. code-block:: python
447
+
448
+ from dataclasses import dataclass
449
+
450
+ from unittest_parametrize import ParametrizedTestCase, parametrize
451
+
452
+
453
+ @dataclass
454
+ class SquareParams:
455
+ x: int
456
+ expected: int
457
+
458
+
459
+ class SquareTests(ParametrizedTestCase):
460
+ @parametrize(
461
+ "sp",
462
+ [
463
+ (SquareParams(x=1, expected=1),),
464
+ (SquareParams(x=2, expected=4),),
465
+ ],
466
+ )
467
+ def test_square(self, sp: SquareParams) -> None:
468
+ self.assertEqual(sp.x**2, sp.expected)
469
+
470
+ This way, each parameter is type-checked and named, improving safety and readability.
471
+
383
472
  History
384
473
  =======
385
474