unittest-parametrize 1.6.0__py3-none-any.whl → 1.8.0__py3-none-any.whl

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.
@@ -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 | Any],
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,15 +153,18 @@ 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)
164
160
  params.append(argvalue)
165
-
161
+ elif len(argnames) == 1:
162
+ argvalue = param(argvalue, id=make_id(i, (argvalue,), ids))
163
+ seen_ids.add(argvalue.id)
164
+ params.append(argvalue)
166
165
  else:
167
166
  raise TypeError(
168
- f"argvalue at index {i} is not a tuple or param instance: {argvalue!r}"
167
+ f"argvalue at index {i} is not a tuple, param instance, or single value: {argvalue!r}"
169
168
  )
170
169
 
171
170
  _parametrized = parametrized(argnames, params)
@@ -183,3 +182,34 @@ def parametrize(
183
182
  return func
184
183
 
185
184
  return wrapper
185
+
186
+
187
+ def make_id(
188
+ i: int,
189
+ argvalue: tuple[Any, ...] | param,
190
+ ids: Sequence[str | None] | Callable[[Any], str | None] | None,
191
+ ) -> str:
192
+ if callable(ids):
193
+ if isinstance(argvalue, tuple):
194
+ values = argvalue
195
+ else:
196
+ values = argvalue.args
197
+
198
+ id_parts = []
199
+ for value in values:
200
+ id_part = ids(value)
201
+ if id_part is not None:
202
+ id_parts.append(id_part)
203
+ else:
204
+ id_parts.append(str(value))
205
+ id_ = "_".join(id_parts)
206
+ # Validate the generated ID
207
+ if not f"_{id_}".isidentifier():
208
+ raise ValueError(
209
+ f"callable ids returned invalid Python identifier suffix: {id_!r}"
210
+ )
211
+ return id_
212
+ elif ids and ids[i]:
213
+ return str(ids[i])
214
+ else:
215
+ 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.8.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
@@ -17,11 +17,13 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
20
21
  Classifier: Typing :: Typed
21
22
  Requires-Python: >=3.9
22
23
  Description-Content-Type: text/x-rst
23
24
  License-File: LICENSE
24
25
  Requires-Dist: typing-extensions; python_version < "3.10"
26
+ Dynamic: license-file
25
27
 
26
28
  ====================
27
29
  unittest-parametrize
@@ -58,7 +60,7 @@ Install with:
58
60
 
59
61
  python -m pip install unittest-parametrize
60
62
 
61
- Python 3.9 to 3.13 supported.
63
+ Python 3.9 to 3.14 supported.
62
64
 
63
65
  Usage
64
66
  =====
@@ -76,15 +78,14 @@ There are two steps to parametrize a test case:
76
78
  2. Apply ``@parametrize`` to any test methods for parametrization.
77
79
  This decorator takes (at least):
78
80
 
79
- * the argument names to parametrize, as comma-separated string
80
- * a list of parameter tuples to create individual tests for
81
+ * the argument names to parametrize, as comma-separated string or sequence of strings.
82
+ * a list of parameters to create individual tests for, which may be tuples, ``param`` objects, or single values (for one argument).
81
83
 
82
84
  Here’s a basic example:
83
85
 
84
86
  .. code-block:: python
85
87
 
86
- from unittest_parametrize import parametrize
87
- from unittest_parametrize import ParametrizedTestCase
88
+ from unittest_parametrize import ParametrizedTestCase, parametrize
88
89
 
89
90
 
90
91
  class SquareTests(ParametrizedTestCase):
@@ -106,6 +107,25 @@ It supports both synchronous and asynchronous test methods.
106
107
  .. |__init_subclass__ hook| replace:: ``__init_subclass__`` hook
107
108
  __ https://docs.python.org/3/reference/datamodel.html#object.__init_subclass__
108
109
 
110
+ Provide a single parameter without a wrapping tuple
111
+ ---------------------------------------------------
112
+
113
+ If you only need a single parameter, you can provide values without wrapping them in tuples:
114
+
115
+ .. code-block:: python
116
+
117
+ from unittest_parametrize import ParametrizedTestCase, parametrize
118
+
119
+
120
+ class EqualTests(ParametrizedTestCase):
121
+ @parametrize(
122
+ "x",
123
+ [1, 2, 3],
124
+ )
125
+ def test_equal(self, x: int) -> None:
126
+ self.assertEqual(x, x)
127
+
128
+
109
129
  Provide argument names as separate strings
110
130
  ------------------------------------------
111
131
 
@@ -113,8 +133,7 @@ You can provide argument names as a sequence of strings instead:
113
133
 
114
134
  .. code-block:: python
115
135
 
116
- from unittest_parametrize import parametrize
117
- from unittest_parametrize import ParametrizedTestCase
136
+ from unittest_parametrize import ParametrizedTestCase, parametrize
118
137
 
119
138
 
120
139
  class SquareTests(ParametrizedTestCase):
@@ -177,13 +196,20 @@ You can see these names when running the tests:
177
196
 
178
197
  OK
179
198
 
180
- You can customize these names by passing ``param`` objects, which contain the arguments and an optional ID for the suffix:
199
+ You can customize these names in several ways:
200
+
201
+ 1. Using ``param`` objects with IDs.
202
+ 2. Passing a sequence of strings as the ``ids`` argument.
203
+ 3. Passing a callable as the ``ids`` argument.
204
+
205
+ Passing ``param`` objects with IDs
206
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
207
+
208
+ Pass a ``param`` object for each parameter set, setting the test ID suffix with the optional ``id`` argument:
181
209
 
182
210
  .. code-block:: python
183
211
 
184
- from unittest_parametrize import param
185
- from unittest_parametrize import parametrize
186
- from unittest_parametrize import ParametrizedTestCase
212
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
187
213
 
188
214
 
189
215
  class SquareTests(ParametrizedTestCase):
@@ -197,7 +223,7 @@ You can customize these names by passing ``param`` objects, which contain the ar
197
223
  def test_square(self, x: int, expected: int) -> None:
198
224
  self.assertEqual(x**2, expected)
199
225
 
200
- Yielding perhaps more natural names:
226
+ Yielding more natural names:
201
227
 
202
228
  .. code-block:: console
203
229
 
@@ -216,9 +242,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
216
242
 
217
243
  .. code-block:: python
218
244
 
219
- from unittest_parametrize import param
220
- from unittest_parametrize import parametrize
221
- from unittest_parametrize import ParametrizedTestCase
245
+ from unittest_parametrize import ParametrizedTestCase, param, parametrize
222
246
 
223
247
 
224
248
  class SquareTests(ParametrizedTestCase):
@@ -232,7 +256,7 @@ Since parameter IDs are optional, you can provide them only for some tests:
232
256
  def test_square(self, x: int, expected: int) -> None:
233
257
  self.assertEqual(x**2, expected)
234
258
 
235
- ID-free ``param``\s fall back to the default index suffixes:
259
+ The ID-free ``param``\s fall back to the default index suffixes:
236
260
 
237
261
  .. code-block:: console
238
262
 
@@ -245,12 +269,14 @@ ID-free ``param``\s fall back to the default index suffixes:
245
269
 
246
270
  OK
247
271
 
248
- Alternatively, you can provide the id’s separately with the ``ids`` argument:
272
+ Passing a sequence of strings as the ``ids`` argument
273
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
274
+
275
+ Another option is to provide the IDs in the separate ``ids`` argument:
249
276
 
250
277
  .. code-block:: python
251
278
 
252
- from unittest_parametrize import parametrize
253
- from unittest_parametrize import ParametrizedTestCase
279
+ from unittest_parametrize import ParametrizedTestCase, parametrize
254
280
 
255
281
 
256
282
  class SquareTests(ParametrizedTestCase):
@@ -265,6 +291,64 @@ Alternatively, you can provide the id’s separately with the ``ids`` argument:
265
291
  def test_square(self, x: int, expected: int) -> None:
266
292
  self.assertEqual(x**2, expected)
267
293
 
294
+ This option sets the full suffixes to the provided strings:
295
+
296
+ .. code-block:: console
297
+
298
+ $ python -m unittest t.py -v
299
+ test_square_one (example.SquareTests.test_square_one) ... ok
300
+ test_square_two (example.SquareTests.test_square_two) ... ok
301
+
302
+ ----------------------------------------------------------------------
303
+ Ran 2 tests in 0.000s
304
+
305
+ OK
306
+
307
+ Passing a callable as the ``ids`` argument
308
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
309
+
310
+ The ``ids`` argument can also be a callable, which unittest-parametrize calls once per parameter value.
311
+ The callable can return a string for that value, or ``None`` to use the default index suffix.
312
+ The values are then joined with underscores to form the full suffix.
313
+
314
+ For example:
315
+
316
+ .. code-block:: python
317
+
318
+ from unittest_parametrize import ParametrizedTestCase, parametrize
319
+
320
+
321
+ def make_id(value):
322
+ if isinstance(value, int):
323
+ return f"num{value}"
324
+ return None
325
+
326
+
327
+ class SquareTests(ParametrizedTestCase):
328
+ @parametrize(
329
+ "x,expected",
330
+ [
331
+ (1, 1),
332
+ (2, 4),
333
+ ],
334
+ ids=make_id,
335
+ )
336
+ def test_square(self, x: int, expected: int) -> None:
337
+ self.assertEqual(x**2, expected)
338
+
339
+ …yields:
340
+
341
+ .. code-block:: console
342
+
343
+ $ python -m unittest t.py -v
344
+ test_square_num1_num1 (example.SquareTests.test_square_num1_num1) ... ok
345
+ test_square_num2_num4 (example.SquareTests.test_square_num2_num4) ... ok
346
+
347
+ ----------------------------------------------------------------------
348
+ Ran 2 tests in 0.000s
349
+
350
+ OK
351
+
268
352
  Use with other test decorators
269
353
  ------------------------------
270
354
 
@@ -275,8 +359,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
275
359
  .. code-block:: python
276
360
 
277
361
  from unittest import mock
278
- from unittest_parametrize import parametrize
279
- from unittest_parametrize import ParametrizedTestCase
362
+ from unittest_parametrize import ParametrizedTestCase, parametrize
280
363
 
281
364
 
282
365
  class CarpentryTests(ParametrizedTestCase):
@@ -285,8 +368,7 @@ So decorators like ``@mock.patch`` need be beneath ``@parametrize``:
285
368
  [(11,), (17,)],
286
369
  )
287
370
  @mock.patch("example.hammer", autospec=True)
288
- def test_nail_a_board(self, mock_hammer, nails):
289
- ...
371
+ def test_nail_a_board(self, mock_hammer, nails): ...
290
372
 
291
373
  Also note that due to how ``mock.patch`` always adds positional arguments at the start, the parametrized arguments must come last.
292
374
  ``@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 +376,7 @@ Also note that due to how ``mock.patch`` always adds positional arguments at the
294
376
  .. code-block:: python
295
377
 
296
378
  # ...
297
- def test_nail_a_board(self, mock_hammer, *, nails):
298
- ...
379
+ def test_nail_a_board(self, mock_hammer, *, nails): ...
299
380
 
300
381
  Multiple ``@parametrize`` decorators
301
382
  ------------------------------------
@@ -305,8 +386,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
305
386
 
306
387
  .. code-block:: python
307
388
 
308
- from unittest_parametrize import parametrize
309
- from unittest_parametrize import ParametrizedTestCase
389
+ from unittest_parametrize import ParametrizedTestCase, parametrize
310
390
 
311
391
 
312
392
  class RocketTests(ParametrizedTestCase):
@@ -318,8 +398,7 @@ To create a cross-product of tests, you can use nested list comprehensions:
318
398
  for hyperdrive_level in [0, 1, 2]
319
399
  ],
320
400
  )
321
- def test_takeoff(self, use_ions, hyperdrive_level) -> None:
322
- ...
401
+ def test_takeoff(self, use_ions, hyperdrive_level) -> None: ...
323
402
 
324
403
  The above creates 2 * 3 = 6 versions of ``test_takeoff``.
325
404
 
@@ -331,8 +410,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
331
410
  .. code-block:: python
332
411
 
333
412
  from itertools import product
334
- from unittest_parametrize import parametrize
335
- from unittest_parametrize import ParametrizedTestCase
413
+ from unittest_parametrize import ParametrizedTestCase, parametrize
336
414
 
337
415
 
338
416
  class RocketTests(ParametrizedTestCase):
@@ -346,8 +424,7 @@ __ https://docs.python.org/3/library/itertools.html#itertools.product
346
424
  )
347
425
  ),
348
426
  )
349
- def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None:
350
- ...
427
+ def test_takeoff(self, use_ions, hyperdrive_level, nose_colour) -> None: ...
351
428
 
352
429
  The above creates 2 * 3 * 2 = 12 versions of ``test_takeoff``.
353
430
 
@@ -371,15 +448,47 @@ To parametrize all tests within a test case, create a separate decorator and app
371
448
 
372
449
  class StatsTests(ParametrizedTestCase):
373
450
  @parametrize_race
374
- def test_strength(self, race: str) -> None:
375
- ...
451
+ def test_strength(self, race: str) -> None: ...
376
452
 
377
453
  @parametrize_race
378
- def test_dexterity(self, race: str) -> None:
379
- ...
454
+ def test_dexterity(self, race: str) -> None: ...
380
455
 
381
456
  ...
382
457
 
458
+ Pass parameters in a dataclass
459
+ ------------------------------
460
+
461
+ Thanks to `Florian Bruhin <https://bruhin.software/>`__ for this tip, from his `pytest tips and tricks presentation <https://bruhin.software/>`__.
462
+
463
+ If your test uses many parameters or cases, the parametrization may become unwieldy, as cases don’t name the arguments.
464
+ In this case, try using a `dataclass <https://docs.python.org/3/library/dataclasses.html>`__ to hold the arguments:
465
+
466
+ .. code-block:: python
467
+
468
+ from dataclasses import dataclass
469
+
470
+ from unittest_parametrize import ParametrizedTestCase, parametrize
471
+
472
+
473
+ @dataclass
474
+ class SquareParams:
475
+ x: int
476
+ expected: int
477
+
478
+
479
+ class SquareTests(ParametrizedTestCase):
480
+ @parametrize(
481
+ "sp",
482
+ [
483
+ (SquareParams(x=1, expected=1),),
484
+ (SquareParams(x=2, expected=4),),
485
+ ],
486
+ )
487
+ def test_square(self, sp: SquareParams) -> None:
488
+ self.assertEqual(sp.x**2, sp.expected)
489
+
490
+ This way, each parameter is type-checked and named, improving safety and readability.
491
+
383
492
  History
384
493
  =======
385
494
 
@@ -0,0 +1,7 @@
1
+ unittest_parametrize/__init__.py,sha256=FP9gc8jyJOHJVe9s52mbMEEsZpyw5vuD3tZxEuLclhg,7433
2
+ unittest_parametrize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ unittest_parametrize-1.8.0.dist-info/licenses/LICENSE,sha256=Jn179RflkRZXhSCGt_D7asLy2Ex47C0WdBHxe0lBazk,1069
4
+ unittest_parametrize-1.8.0.dist-info/METADATA,sha256=6I1nJVKypKpSdAlx9npkXSVIVN2Y1CM8j4rjey5r_So,18449
5
+ unittest_parametrize-1.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ unittest_parametrize-1.8.0.dist-info/top_level.txt,sha256=4nLqNaGtBX8Ny36ZdFt37Gk-87hPtOvfdfZlTBi3OtU,21
7
+ unittest_parametrize-1.8.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.7.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,7 +0,0 @@
1
- unittest_parametrize/__init__.py,sha256=-WNbay-GoS2_ENp9cOgE3pf_BJH2Nr1eX7wiAwJyxic,6299
2
- unittest_parametrize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- unittest_parametrize-1.6.0.dist-info/LICENSE,sha256=Jn179RflkRZXhSCGt_D7asLy2Ex47C0WdBHxe0lBazk,1069
4
- unittest_parametrize-1.6.0.dist-info/METADATA,sha256=kZJe0NHStrHyHJgkttwwmGgBOagnfB4XfffeK_SspUI,15223
5
- unittest_parametrize-1.6.0.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
6
- unittest_parametrize-1.6.0.dist-info/top_level.txt,sha256=4nLqNaGtBX8Ny36ZdFt37Gk-87hPtOvfdfZlTBi3OtU,21
7
- unittest_parametrize-1.6.0.dist-info/RECORD,,