-
Notifications
You must be signed in to change notification settings - Fork 41
/
fixture_parametrize_plus.py
1380 lines (1155 loc) · 67 KB
/
fixture_parametrize_plus.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Authors: Sylvain MARIE <sylvain.marie@se.com>
# + All contributors to <https://github.com/smarie/python-pytest-cases>
#
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
from inspect import isgeneratorfunction
from warnings import warn
try: # python 3.3+
from inspect import signature, Parameter
except ImportError:
from funcsigs import signature, Parameter # noqa
try:
from collections.abc import Iterable
except ImportError: # noqa
from collections import Iterable
try:
from typing import Union, Callable, List, Any, Sequence, Optional, Type, Tuple # noqa
from types import ModuleType # noqa
except ImportError:
pass
import pytest
from makefun import with_signature, remove_signature_parameters, add_signature_parameters, wraps
from .common_mini_six import string_types
from .common_others import AUTO, robust_isinstance, replace_list_contents
from .common_pytest_marks import has_pytest_param, get_param_argnames_as_list
from .common_pytest_lazy_values import is_lazy_value, get_lazy_args
from .common_pytest import get_fixture_name, remove_duplicates, mini_idvalset, is_marked_parameter_value, \
extract_parameterset_info, ParameterSet, cart_product_pytest, mini_idval, inject_host, \
get_marked_parameter_values, resolve_ids, get_marked_parameter_id, get_marked_parameter_marks, is_fixture, \
safe_isclass
from .fixture__creation import check_name_available, CHANGE, WARN
from .fixture_core1_unions import InvalidParamsList, NOT_USED, UnionFixtureAlternative, _make_fixture_union, \
_make_unpack_fixture, UnionIdMakers
from .fixture_core2 import _create_param_fixture, fixture
def _fixture_product(fixtures_dest,
name, # type: str
fixtures_or_values,
fixture_positions,
scope="function", # type: str
unpack_into=None, # type: Iterable[str]
autouse=False, # type: bool
hook=None, # type: Callable[[Callable], Callable]
caller=None, # type: Callable
**kwargs):
"""
Internal implementation for fixture products created by pytest parametrize plus.
:param fixtures_dest:
:param name:
:param fixtures_or_values:
:param fixture_positions:
:param idstyle:
:param scope:
:param ids:
:param unpack_into:
:param autouse:
:param kwargs:
:return:
"""
# test the `fixtures` argument to avoid common mistakes
if not isinstance(fixtures_or_values, (tuple, set, list)):
raise TypeError("fixture_product: the `fixtures_or_values` argument should be a tuple, set or list")
else:
has_lazy_vals = any(is_lazy_value(v) for v in fixtures_or_values)
_tuple_size = len(fixtures_or_values)
# first get all required fixture names
f_names = [None] * _tuple_size
for f_pos in fixture_positions:
# possibly get the fixture name if the fixture symbol was provided
f = fixtures_or_values[f_pos]
if isinstance(f, fixture_ref):
f = f.fixture
# and remember the position in the tuple
f_names[f_pos] = get_fixture_name(f)
# remove duplicates by making it an ordered set
all_names = remove_duplicates((n for n in f_names if n is not None))
if len(all_names) < 1:
raise ValueError("Empty fixture products are not permitted")
def _tuple_generator(request, all_fixtures):
for i in range(_tuple_size):
fix_at_pos_i = f_names[i]
if fix_at_pos_i is None:
# fixed value
# note: wouldnt it be almost as efficient but more readable to *always* call handle_lazy_args?
yield get_lazy_args(fixtures_or_values[i], request) if has_lazy_vals else fixtures_or_values[i]
else:
# fixture value
yield all_fixtures[fix_at_pos_i]
# then generate the body of our product fixture. It will require all of its dependent fixtures
@with_signature("(request, %s)" % ', '.join(all_names))
def _new_fixture(request, **all_fixtures):
return tuple(_tuple_generator(request, all_fixtures))
_new_fixture.__name__ = name
# finally create the fixture per se.
# WARNING we do not use pytest.fixture but fixture so that NOT_USED is discarded
f_decorator = fixture(scope=scope, autouse=autouse, hook=hook, **kwargs)
fix = f_decorator(_new_fixture)
# Dynamically add fixture to caller's module as explained in https://github.com/pytest-dev/pytest/issues/2424
check_name_available(fixtures_dest, name, if_name_exists=WARN, caller=caller)
setattr(fixtures_dest, name, fix)
# if unpacking is requested, do it here
if unpack_into is not None:
# note that as for fixture unions, we can not expose the `in_cls` parameter.
# but there is an easy workaround if unpacking is needed: call unpack_fixture separately
_make_unpack_fixture(fixtures_dest, argnames=unpack_into, fixture=name, hook=hook, in_cls=False)
return fix
_make_fixture_product = _fixture_product
"""A readable alias for callers not using the returned symbol"""
class fixture_ref(object): # noqa
"""
A reference to a fixture, to be used in `@parametrize`.
You can create it from a fixture name or a fixture object (function).
"""
__slots__ = 'fixture', 'theoretical_size', '_id'
def __init__(self,
fixture, # type: Union[str, Callable]
id=None, # type: str # noqa
):
"""
:param fixture: the name of the fixture to reference, or the fixture function itself
:param id: an optional custom id to override the fixture name in ids.
"""
self.fixture = get_fixture_name(fixture)
self._id = id
self.theoretical_size = None # we dont know yet, will be filled by @parametrize
def get_name_for_id(self):
"""return the name to use in ids."""
return self._id if self._id is not None else self.fixture
def __str__(self):
# used in mini_idval for example
return self.get_name_for_id()
def __repr__(self):
if self._id is not None:
return "fixture_ref<%s, id=%s>" % (self.fixture, self._id)
else:
return "fixture_ref<%s>" % self.fixture
def _check_iterable(self):
"""Raise a TypeError if this fixture reference is not iterable, that is, it does not represent a tuple"""
if self.theoretical_size is None:
raise TypeError("This `fixture_ref` has not yet been initialized, so it cannot be unpacked/iterated upon. "
"This is not supposed to happen when a `fixture_ref` is used correctly, i.e. as an item in"
" the `argvalues` of a `@parametrize` decorator. Please check the documentation for "
"details.")
if self.theoretical_size == 1:
raise TypeError("This fixture_ref does not represent a tuple of arguments, it is not iterable")
def __len__(self):
self._check_iterable()
return self.theoretical_size
def __getitem__(self, item):
"""
Returns an item in the tuple described by this fixture_ref.
This is just a facade, a FixtureRefItem.
Note: this is only used when a custom `idgen` is passed to @parametrized
"""
self._check_iterable()
return FixtureRefItem(self, item)
class FixtureRefItem(object):
"""An item in a fixture_ref when this fixture_ref is used as a tuple."""
__slots__ = 'host', 'item'
def __init__(self,
host, # type: fixture_ref
item # type: int
):
self.host = host
self.item = item
def __repr__(self):
return "%r[%s]" % (self.host, self.item)
# Fix for https://github.com/smarie/python-pytest-cases/issues/71
# In order for pytest to allow users to import this symbol in conftest.py
# they should be declared as optional plugin hooks.
# A workaround otherwise would be to remove the 'pytest_' name prefix
# See https://github.com/pytest-dev/pytest/issues/6475
@pytest.hookimpl(optionalhook=True)
def pytest_parametrize_plus(*args,
**kwargs):
warn("`pytest_parametrize_plus` and `parametrize_plus` are deprecated. Please use the new alias `parametrize`. "
"See https://github.com/pytest-dev/pytest/issues/6475", category=DeprecationWarning, stacklevel=2)
return parametrize(*args, **kwargs)
parametrize_plus = pytest_parametrize_plus
class ParamAlternative(UnionFixtureAlternative):
"""Defines an "alternative", used to parametrize a fixture union in the context of parametrize
It is similar to a union fixture alternative, except that it also remembers the parameter argnames.
They are used to generate the test id corresponding to this alternative. See `_get_minimal_id` implementations.
`ParamIdMakers` overrides some of the idstyles in `UnionIdMakers` so as to adapt them to these `ParamAlternative`
objects.
"""
__slots__ = ('argnames', 'decorated')
def __init__(self,
union_name, # type: str
alternative_name, # type: str
param_index, # type: int
argnames, # type: Sequence[str]
decorated # type: Callable
):
"""
:param union_name: the name of the union fixture created by @parametrize to switch between param alternatives
:param alternative_name: the name of the fixture created by @parametrize to represent this alternative
:param param_index: the index of this parameter in the list of argvalues passed to @parametrize
:param argnames: the list of parameter names in @parametrize
:param decorated: the test function or fixture that this alternative refers to
"""
super(ParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name,
alternative_index=param_index)
self.argnames = argnames
self.decorated = decorated
def get_union_id(self):
return ("(%s)" % ",".join(self.argnames)) if len(self.argnames) > 1 else self.argnames[0]
def get_alternative_idx(self):
return "P%s" % self.alternative_index
def get_alternative_id(self):
"""Subclasses should return the smallest id representing this parametrize fixture union alternative"""
raise NotImplementedError()
class SingleParamAlternative(ParamAlternative):
"""alternative class for single parameter value"""
__slots__ = 'argval', 'id'
def __init__(self,
union_name, # type: str
alternative_name, # type: str
param_index, # type: int
argnames, # type: Sequence[str]
argval, # type: Any
id, # type: Optional[str]
decorated # type: Callable
):
"""
:param union_name: the name of the union fixture created by @parametrize to switch between param alternatives
:param alternative_name: the name of the fixture created by @parametrize to represent this alternative
:param param_index: the index of this parameter in the list of argvalues passed to @parametrize
:param argnames: the list of parameter names in @parametrize
:param argval: the value used by this parameter
"""
super(SingleParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name,
param_index=param_index, argnames=argnames, decorated=decorated)
self.argval = argval
self.id = id
def get_alternative_id(self):
"""Since this alternative has no further parametrization (simplification for 1-param alternative),
we create here the equivalent of the id of the argvalue if it was used as a parameter"""
if self.id is not None:
# custom id from `@parametrize(ids=<callable_or_list>)`
return self.id
else:
return mini_idvalset(self.argnames, self.argval, idx=self.alternative_index)
@classmethod
def create(cls,
new_fixture_host, # type: Union[Type, ModuleType]
test_func, # type: Callable
param_union_name, # type: str
argnames, # type: Sequence[str]
i, # type: int
argvalue, # type: Any
id, # type: Union[str, Callable]
hook=None, # type: Callable
debug=False # type: bool
):
# type: (...) -> SingleParamAlternative
"""
Creates an alternative for fixture union `param_union_name`, to handle single parameter value
argvalue = argvalues[i] in @parametrize.
This alternative will refer to a newly created fixture in `new_fixture_host`, that will return `argvalue`.
:param new_fixture_host: host (class, module) where the new fixture should be created
:param test_func:
:param param_union_name:
:param argnames:
:param i:
:param argvalue: a (possibly marked with pytest.param) argvalue
:param hook:
:param debug:
:return:
"""
nb_params = len(argnames)
param_names_str = '_'.join(argnames).replace(' ', '')
# Create a unique fixture name
p_fix_name = "%s_%s_P%s" % (test_func.__name__, param_names_str, i)
p_fix_name = check_name_available(new_fixture_host, p_fix_name, if_name_exists=CHANGE, caller=parametrize)
if debug:
print(" - Creating new fixture %r to handle parameter %s" % (p_fix_name, i))
# Now we'll create the fixture that will return the unique parameter value
# since this parameter is unique, we do not parametrize the fixture (_create_param_fixture "auto_simplify" flag)
# for this reason the possible pytest.param ids and marks have to be set somewhere else: we move them
# to the alternative.
# unwrap possible pytest.param on the argvalue to move them on the SingleParamAlternative
has_pytestparam_wrapper = is_marked_parameter_value(argvalue)
if has_pytestparam_wrapper:
p_id = get_marked_parameter_id(argvalue)
p_marks = get_marked_parameter_marks(argvalue)
argvalue = get_marked_parameter_values(argvalue, nbargs=nb_params)
if nb_params == 1:
argvalue = argvalue[0]
# Create the fixture. IMPORTANT auto_simplify=True : we create a NON-parametrized fixture.
_create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=(argvalue,),
hook=hook, auto_simplify=True, debug=debug)
# Create the alternative
argvals = (argvalue,) if nb_params == 1 else argvalue
p_fix_alt = SingleParamAlternative(union_name=param_union_name, alternative_name=p_fix_name,
argnames=argnames, param_index=i, argval=argvals, id=id,
decorated=test_func)
# Finally copy the custom id/marks on the ParamAlternative if any
if has_pytestparam_wrapper:
p_fix_alt = ParameterSet(values=(p_fix_alt,), id=p_id, marks=p_marks) # noqa
return p_fix_alt
class MultiParamAlternative(ParamAlternative):
"""alternative class for multiple parameter values"""
__slots__ = 'param_index_from', 'param_index_to'
def __init__(self,
union_name, # type: str
alternative_name, # type: str
argnames, # type: Sequence[str]
param_index_from, # type: int
param_index_to, # type: int
decorated # type: Callable
):
"""
:param union_name: the name of the union fixture created by @parametrize to switch between param alternatives
:param alternative_name: the name of the fixture created by @parametrize to represent this alternative
:param argnames: the list of parameter names in @parametrize
:param param_index_from: the beginning index of the parameters covered by <alternative_name> in the list of
argvalues passed to @parametrize
:param param_index_to: the ending index of the parameters covered by <alternative_name> in the list of
argvalues passed to @parametrize
"""
# set the param_index to be None since here we represent several indices
super(MultiParamAlternative, self).__init__(union_name=union_name, alternative_name=alternative_name,
argnames=argnames, param_index=None, decorated=decorated # noqa
)
self.param_index_from = param_index_from
self.param_index_to = param_index_to
def __str__(self):
return "%s/%s/" % (self.get_union_id(), self.get_alternative_idx())
def get_alternative_idx(self):
return "P%s:%s" % (self.param_index_from, self.param_index_to)
def get_alternative_id(self):
# The alternative id is the parameter range - the parameter themselves appear on the referenced fixture
return self.get_alternative_idx()
@classmethod
def create(cls,
new_fixture_host, # type: Union[Type, ModuleType]
test_func, # type: Callable
param_union_name, # type: str
argnames, # type: Sequence[str]
from_i, # type: int
to_i, # type: int
argvalues, # type: Any
ids, # type: Union[Sequence[str], Callable]
hook=None, # type: Callable
debug=False # type: bool
):
# type: (...) -> MultiParamAlternative
"""
Creates an alternative for fixture union `param_union_name`, to handle a group of consecutive parameters
argvalues[from_i:to_i] in @parametrize. Note that here the received `argvalues` should be already sliced
This alternative will refer to a newly created fixture in `new_fixture_host`, that will be parametrized to
return each of `argvalues`.
:param new_fixture_host:
:param test_func:
:param param_union_name:
:param argnames:
:param from_i:
:param to_i:
:param argvalues:
:param hook:
:param debug:
:return:
"""
nb_params = len(argnames)
param_names_str = '_'.join(argnames).replace(' ', '')
# Create a unique fixture name
p_fix_name = "%s_%s_is_P%stoP%s" % (test_func.__name__, param_names_str, from_i, to_i - 1)
p_fix_name = check_name_available(new_fixture_host, p_fix_name, if_name_exists=CHANGE, caller=parametrize)
if debug:
print(" - Creating new fixture %r to handle parameters %s to %s" % (p_fix_name, from_i, to_i - 1))
# Create the fixture
# - it will be parametrized to take all the values in argvalues
# - therefore it will use the custom ids and marks if any
# - it will be unique (not unfolded) so if there are more than 1 argnames we have to add a layer of tuple in the
# values
if nb_params > 1:
# we have to create a tuple around the vals because we have a SINGLE parameter that is a tuple
unmarked_argvalues = []
new_argvals = []
for v in argvalues:
if is_marked_parameter_value(v):
# transform the parameterset so that it contains a tuple of length 1
vals = get_marked_parameter_values(v, nbargs=nb_params)
if nb_params == 1:
vals = vals[0]
unmarked_argvalues.append(vals)
new_argvals.append(ParameterSet((vals,),
id=get_marked_parameter_id(v),
marks=get_marked_parameter_marks(v)))
else:
# nothing special to do since there is no pytest.param here
new_argvals.append(v)
unmarked_argvalues.append(v)
argvalues = new_argvals
# we also have to generate the ids correctly "as if they were multiple"
try:
iter(ids)
except TypeError:
if ids is not None:
ids = ["-".join(ids(vi) for vi in v) for v in unmarked_argvalues]
else:
ids = [mini_idvalset(argnames, vals, i) for i, vals in enumerate(unmarked_argvalues)]
_create_param_fixture(new_fixture_host, argname=p_fix_name, argvalues=argvalues, ids=ids, hook=hook,
debug=debug)
# Create the corresponding alternative
# note: as opposed to SingleParamAlternative, no need to move the custom id/marks to the ParamAlternative
# since they are set on the created parametrized fixture above
return MultiParamAlternative(union_name=param_union_name, alternative_name=p_fix_name, argnames=argnames,
param_index_from=from_i, param_index_to=to_i, decorated=test_func)
class FixtureParamAlternative(SingleParamAlternative):
"""alternative class for a single parameter containing a fixture ref"""
def __init__(self,
union_name, # type: str
fixture_ref, # type: fixture_ref
argnames, # type: Sequence[str]
param_index, # type: int
id, # type: Optional[str]
decorated # type: Callable
):
"""
:param union_name: the name of the union fixture created by @parametrize to switch between param alternatives
:param param_index: the index of this parameter in the list of argvalues passed to @parametrize
:param argnames: the list of parameter names in @parametrize
:param fixture_ref: the fixture reference used in this alternative
"""
# set alternative_name using the fixture name in fixture_ref
super(FixtureParamAlternative, self).__init__(union_name=union_name,
alternative_name=fixture_ref.fixture,
argnames=argnames, param_index=param_index,
argval=fixture_ref, id=id, decorated=decorated)
def get_alternative_idx(self):
return "P%sF" % self.alternative_index
def get_alternative_id(self):
if self.id is not None:
# custom id from `@parametrize(ids=<callable_or_list>)`
return self.id
else:
# ask the fixture_ref for an id: it can be the fixture name or a custom id
return self.argval.get_name_for_id()
class ProductParamAlternative(SingleParamAlternative):
"""alternative class for a single product parameter containing fixture refs"""
def get_alternative_idx(self):
return "P%sF" % self.alternative_index
def get_alternative_id(self):
"""Similar to SingleParamAlternative: create an id representing this tuple, since the fixture wont be
parametrized"""
if self.id is not None:
# custom id from `@parametrize(ids=<callable_or_list>)`
return self.id
else:
argval = tuple(t if not robust_isinstance(t, fixture_ref) else t.get_name_for_id() for t in self.argval)
return mini_idvalset(self.argnames, argval, idx=self.alternative_index)
# if PYTEST54_OR_GREATER:
# # an empty string will be taken into account but NOT filtered out in CallSpec2.id.
# # so instead we create a dedicated unique string and return it.
# # Ugly but the only viable alternative seems worse: it would be to return an empty string
# # and in `remove_empty_ids` to always remove all empty strings (not necessary the ones set by us).
# # That is too much of a change.
EMPTY_ID = "<pytest_cases_empty_id>"
if has_pytest_param:
def remove_empty_ids(callspec):
# used by plugin.py to remove the EMPTY_ID from the callspecs
replace_list_contents(callspec._idlist, [c for c in callspec._idlist if not c.startswith(EMPTY_ID)])
else:
def remove_empty_ids(callspec):
# used by plugin.py to remove the EMPTY_ID from the callspecs
replace_list_contents(callspec._idlist, [c for c in callspec._idlist if not c.endswith(EMPTY_ID)])
# elif PYTEST421_OR_GREATER:
# # an empty string will be taken into account and filtered out in CallSpec2.id.
# # but.... if this empty string appears several times in the tests it is appended with a number to become unique :(
# EMPTY_ID = ""
#
# else:
# # an empty string will only be taken into account if its truth value is True
# # but.... if this empty string appears several times in the tests it is appended with a number to become unique :(
# # it will be filtered out in CallSpec2.id
# class EmptyId(str):
# def __new__(cls):
# return str.__new__(cls, "")
#
# def __nonzero__(self):
# # python 2
# return True
#
# def __bool__(self):
# # python 3
# return True
#
# EMPTY_ID = EmptyId()
class ParamIdMakers(UnionIdMakers):
""" 'Enum' of id styles for param ids
It extends UnionIdMakers to adapt to the special fixture alternatives `ParamAlternative` we create
in @parametrize
"""
@classmethod
def nostyle(cls,
param # type: ParamAlternative
):
if isinstance(param, MultiParamAlternative):
# make an empty minimal id since the parameter themselves will appear as ids separately
# note if the final id is empty it will be dropped by the filter in CallSpec2.id
return EMPTY_ID
else:
return UnionIdMakers.nostyle(param)
# @classmethod
# def explicit(cls,
# param # type: ParamAlternative
# ):
# """Same than parent but display the argnames as prefix instead of the fixture union name generated by
# @parametrize, because the latter is too complex (for unicity reasons)"""
# return "%s/%s" % (, param.get_id(prepend_index=True))
_IDGEN = object()
def parametrize(argnames=None, # type: Union[str, Tuple[str], List[str]]
argvalues=None, # type: Iterable[Any]
indirect=False, # type: bool
ids=None, # type: Union[Callable, Iterable[str]]
idstyle=None, # type: Union[str, Callable]
idgen=_IDGEN, # type: Union[str, Callable]
auto_refs=True, # type: bool
scope=None, # type: str
hook=None, # type: Callable[[Callable], Callable]
debug=False, # type: bool
**args):
"""
Equivalent to `@pytest.mark.parametrize` but also supports
(1) new alternate style for argnames/argvalues. One can also use `**args` to pass additional `{argnames: argvalues}`
in the same parametrization call. This can be handy in combination with `idgen` to master the whole id template
associated with several parameters. Note that you can pass coma-separated argnames too, by de-referencing a dict:
e.g. `**{'a,b': [(0, True), (1, False)], 'c': [-1, 2]}`.
(2) new alternate style for ids. One can use `idgen` instead of `ids`. `idgen` can be a callable receiving all
parameters at once (`**args`) and returning an id ; or it can be a string template using the new-style string
formatting where the argnames can be used as variables (e.g. `idgen=lambda **args: "a={a}".format(**args)` or
`idgen="my_id where a={a}"`). The special `idgen=AUTO` symbol can be used to generate a default string template
equivalent to `lambda **args: "-".join("%s=%s" % (n, v) for n, v in args.items())`. This is enabled by default
if you use the alternate style for argnames/argvalues (e.g. if `len(args) > 0`), and if there are no `fixture_ref`s
in your argvalues.
(3) new possibilities in argvalues:
- one can include references to fixtures with `fixture_ref(<fixture>)` where <fixture> can be the fixture name or
fixture function. When such a fixture reference is detected in the argvalues, a new function-scope "union"
fixture will be created with a unique name, and the test function will be wrapped so as to be injected with the
correct parameters from this fixture. Special test ids will be created to illustrate the switching between the
various normal parameters and fixtures. You can see debug print messages about all fixtures created using
`debug=True`
- one can include lazy argvalues with `lazy_value(<valuegetter>, [id=..., marks=...])`. A `lazy_value` is the same
thing than a function-scoped fixture, except that the value getter function is not a fixture and therefore can
neither be parametrized nor depend on fixtures. It should have no mandatory argument.
Both `fixture_ref` and `lazy_value` can be used to represent a single argvalue, or a whole tuple of argvalues when
there are several argnames. Several of them can be used in a tuple.
Finally, `pytest.param` is supported even when there are `fixture_ref` and `lazy_value`.
An optional `hook` can be passed, to apply on each fixture function that is created during this call. The hook
function will be called everytime a fixture is about to be created. It will receive a single argument (the
function implementing the fixture) and should return the function to use. For example you can use `saved_fixture`
from `pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
:param argnames: same as in pytest.mark.parametrize
:param argvalues: same as in pytest.mark.parametrize except that `fixture_ref` and `lazy_value` are supported
:param indirect: same as in pytest.mark.parametrize. Note that it is not recommended and is not guaranteed to work
in complex parametrization scenarii.
:param ids: same as in pytest.mark.parametrize. Note that an alternative way to create ids exists with `idgen`. Only
one non-None `ids` or `idgen should be provided.
:param idgen: an id formatter. Either a string representing a template, or a callable receiving all argvalues
at once (as opposed to the behaviour in pytest ids). This alternative way to generate ids can only be used when
`ids` is not provided (None). You can use the special `AUTO` formatter to generate an automatic id with
template <name>=<value>-<name2>=<value2>-etc. `AUTO` is enabled by default if you use the alternate style for
argnames/argvalues (e.g. if `len(args) > 0`), and if there are no `fixture_ref`s in your argvalues.
:param auto_refs: a boolean. If this is `True` (default), argvalues containing fixture symbols will automatically
be wrapped into a `fixture_ref`, for convenience.
:param idstyle: This is mostly for debug. Style of ids to be used in the "union" fixtures generated by
`@parametrize` if at least one `fixture_ref` is found in the argvalues. `idstyle` possible values are
'compact', 'explicit' or None/'nostyle' (default), or a callable. `idstyle` has no effect if no `fixture_ref`
are present in the argvalues. As opposed to `ids`, a callable provided here will receive a `ParamAlternative`
object indicating which generated fixture should be used. See `ParamIdMakers`.
:param scope: The scope of the union fixture to create if `fixture_ref`s are found in the argvalues. Otherwise same
as in pytest.mark.parametrize.
:param hook: an optional hook to apply to each fixture function that is created during this call. The hook function
will be called everytime a fixture is about to be created. It will receive a single argument (the function
implementing the fixture) and should return the function to use. For example you can use `saved_fixture` from
`pytest-harvest` as a hook in order to save all such created fixtures in the fixture store.
:param debug: print debug messages on stdout to analyze fixture creation (use pytest -s to see them)
:param args: additional {argnames: argvalues} definition
:return:
"""
_decorate, needs_inject = _parametrize_plus(argnames, argvalues, indirect=indirect, ids=ids, idgen=idgen,
auto_refs=auto_refs, idstyle=idstyle, scope=scope,
hook=hook, debug=debug, **args)
if needs_inject:
@inject_host
def _apply_parametrize_plus(f, host_class_or_module):
return _decorate(f, host_class_or_module)
return _apply_parametrize_plus
else:
return _decorate
class InvalidIdTemplateException(Exception):
"""
Raised when a string template provided in an `idgen` raises an error
"""
def __init__(self, idgen, params, caught):
self.idgen = idgen
self.params = params
self.caught = caught
super(InvalidIdTemplateException, self).__init__()
def __str__(self):
return repr(self)
def __repr__(self):
return "Error generating test id using name template '%s' with parameter values " \
"%r. Please check the name template. Caught: %s - %s" \
% (self.idgen, self.params, self.caught.__class__, self.caught)
def _parametrize_plus(argnames=None, # type: Union[str, Tuple[str], List[str]]
argvalues=None, # type: Iterable[Any]
indirect=False, # type: bool
ids=None, # type: Union[Callable, Iterable[str]]
idstyle=None, # type: Optional[Union[str, Callable]]
idgen=_IDGEN, # type: Union[str, Callable]
auto_refs=True, # type: bool
scope=None, # type: str
hook=None, # type: Callable[[Callable], Callable]
debug=False, # type: bool
**args):
"""
:return: a tuple (decorator, needs_inject) where needs_inject is True if decorator has signature (f, host)
and False if decorator has signature (f)
"""
# first handle argnames / argvalues (new modes of input)
argnames, argvalues = _get_argnames_argvalues(argnames, argvalues, **args)
# argnames related
initial_argnames = ','.join(argnames)
nb_params = len(argnames)
# extract all marks and custom ids.
# Do not check consistency of sizes argname/argvalue as a fixture_ref can stand for several argvalues.
marked_argvalues = argvalues
has_cust_ids = (idgen is not _IDGEN or len(args) > 0) or (ids is not None)
p_ids, p_marks, argvalues, fixture_indices, mod_lvid_indices = \
_process_argvalues(argnames, marked_argvalues, nb_params, has_cust_ids, auto_refs=auto_refs)
# idgen default
if idgen is _IDGEN:
# default: use the new id style only when some keyword **args are provided and there are no fixture refs
idgen = AUTO if (len(args) > 0 and len(fixture_indices) == 0 and ids is None) else None
if idgen is AUTO:
# note: we use a "trick" here with mini_idval to get the appropriate result (argname='', idx=v)
def _make_ids(**args):
for n, v in args.items():
yield "%s=%s" % (n, mini_idval(val=v, argname='', idx=v))
idgen = lambda **args: "-".join(_make_ids(**args)) # noqa
# generate id
if idgen is not None:
if ids is not None:
raise ValueError("Only one of `ids` and `idgen` should be provided")
ids = _gen_ids(argnames, argvalues, idgen)
if len(fixture_indices) == 0:
# No fixture refernce: fallback to a standard pytest.mark.parametrize
if debug:
print("No fixture reference found. Calling @pytest.mark.parametrize...")
print(" - argnames: %s" % initial_argnames)
print(" - argvalues: %s" % marked_argvalues)
print(" - ids: %s" % ids)
# handle infinite iterables like latest pytest, for convenience
ids = resolve_ids(ids, marked_argvalues, full_resolve=False)
# no fixture reference: shortcut, do as usual (note that the hook wont be called since no fixture is created)
_decorator = pytest.mark.parametrize(initial_argnames, marked_argvalues, indirect=indirect,
ids=ids, scope=scope)
if indirect:
return _decorator, False
else:
# wrap the decorator to check if the test function has the parameters as arguments
def _apply(test_func):
if not safe_isclass(test_func):
# a Function: raise a proper error message if improper use
s = signature(test_func)
for p in argnames:
if p not in s.parameters:
raise ValueError("parameter '%s' not found in test function signature '%s%s'"
"" % (p, test_func.__name__, s))
else:
# a Class: we cannot really perform any check.
pass
return _decorator(test_func)
return _apply, False
else:
# there are fixture references: we will create a specific decorator replacing the params with a "union" fixture
if indirect:
warn("Using `indirect=True` at the same time as fixture references in `@parametrize` is not guaranteed to "
"work and is strongly discouraged for readability reasons. See "
"https://github.com/smarie/python-pytest-cases/issues/150")
# First unset the pytest.param id we have set earlier in _process_argvalues: indeed it is only needed in
# the case above where we were defaulting to legacy @pytest.mark.parametrize .
# Here we have fixture refs so we will create a fixture union with several ParamAlternative, and their id will
# anyway be generated with `mini_idvalset` which tackles the case of lazy_value used for a tuple of args
for i in mod_lvid_indices:
p_ids[i] = None
if p_marks[i]:
marked_argvalues[i] = ParameterSet(values=marked_argvalues[i].values, id=None, marks=p_marks[i])
else:
marked_argvalues[i] = argvalues[i] # we can even remove the pytest.param wrapper
if indirect:
raise ValueError("Setting `indirect=True` is not yet supported when at least a `fixure_ref` is present in "
"the `argvalues`.")
if debug:
print("Fixture references found. Creating references and fixtures...")
param_names_str = '_'.join(argnames).replace(' ', '')
# Are there explicit ids provided ?
explicit_ids_to_use = False
ids = resolve_ids(ids, argvalues, full_resolve=False)
if isinstance(ids, list):
explicit_ids_to_use = True
# First define a few functions that will help us create the various fixtures to use in the final "union"
def _create_params_alt(fh, test_func, union_name, from_i, to_i, hook): # noqa
""" Routine that will be used to create a parameter fixture for argvalues between prev_i and i"""
# is this about a single value or several values ?
if to_i == from_i + 1:
i = from_i
del from_i
# If an explicit list of ids was provided, slice it. Otherwise use the provided callable
if ids is not None:
_id = ids[i] if explicit_ids_to_use else ids(argvalues[i])
else:
_id = None
return SingleParamAlternative.create(new_fixture_host=fh, test_func=test_func,
param_union_name=union_name, argnames=argnames, i=i,
argvalue=marked_argvalues[i], id=_id,
hook=hook, debug=debug)
else:
# If an explicit list of ids was provided, slice it. Otherwise the provided callable will be used later
_ids = ids[from_i:to_i] if explicit_ids_to_use else ids
return MultiParamAlternative.create(new_fixture_host=fh, test_func=test_func,
param_union_name=union_name, argnames=argnames, from_i=from_i,
to_i=to_i, argvalues=marked_argvalues[from_i:to_i], ids=_ids,
hook=hook, debug=debug)
def _create_fixture_ref_alt(union_name, test_func, i): # noqa
# If an explicit list of ids was provided, slice it. Otherwise use the provided callable
if ids is not None:
_id = ids[i] if explicit_ids_to_use else ids(argvalues[i])
else:
_id = None
# Get the referenced fixture name
f_fix_name = argvalues[i].fixture
if debug:
print(" - Creating reference to existing fixture %r" % (f_fix_name,))
# Create the alternative
f_fix_alt = FixtureParamAlternative(union_name=union_name, fixture_ref=argvalues[i],
decorated=test_func, argnames=argnames, param_index=i, id=_id)
# Finally copy the custom id/marks on the FixtureParamAlternative if any
if is_marked_parameter_value(marked_argvalues[i]):
f_fix_alt = ParameterSet(values=(f_fix_alt,),
id=get_marked_parameter_id(marked_argvalues[i]),
marks=get_marked_parameter_marks(marked_argvalues[i]))
return f_fix_alt
def _create_fixture_ref_product(fh, union_name, i, fixture_ref_positions, test_func, hook): # noqa
# If an explicit list of ids was provided, slice it. Otherwise the provided callable will be used
_id = ids[i] if explicit_ids_to_use else ids
# values to use:
param_values = argvalues[i]
# Create a unique fixture name
p_fix_name = "%s_%s_P%s" % (test_func.__name__, param_names_str, i)
p_fix_name = check_name_available(fh, p_fix_name, if_name_exists=CHANGE, caller=parametrize)
if debug:
print(" - Creating new fixture %r to handle parameter %s that is a cross-product" % (p_fix_name, i))
# Create the fixture
_make_fixture_product(fh, name=p_fix_name, hook=hook, caller=parametrize,
fixtures_or_values=param_values, fixture_positions=fixture_ref_positions)
# Create the corresponding alternative
p_fix_alt = ProductParamAlternative(union_name=union_name, alternative_name=p_fix_name, decorated=test_func,
argval=argvalues[i], argnames=argnames, param_index=i, id=_id)
# copy the custom id/marks to the ParamAlternative if any
if is_marked_parameter_value(marked_argvalues[i]):
p_fix_alt = ParameterSet(values=(p_fix_alt,),
id=get_marked_parameter_id(marked_argvalues[i]),
marks=get_marked_parameter_marks(marked_argvalues[i]))
return p_fix_alt
# Then create the decorator per se
def parametrize_plus_decorate(test_func, fixtures_dest):
"""
A decorator that wraps the test function so that instead of receiving the parameter names, it receives the
new fixture. All other decorations are unchanged.
:param test_func:
:return:
"""
test_func_name = test_func.__name__
# first check if the test function has the parameters as arguments
if safe_isclass(test_func):
# a test class: not supported yet
raise NotImplementedError("@parametrize can not be used to decorate a Test class when the argvalues "
"contain at least one reference to a fixture.")
old_sig = signature(test_func)
for p in argnames:
if p not in old_sig.parameters:
raise ValueError("parameter '%s' not found in test function signature '%s%s'"
"" % (p, test_func_name, old_sig))
# The name for the final "union" fixture
# style_template = "%s_param__%s"
main_fixture_style_template = "%s_%s"
fixture_union_name = main_fixture_style_template % (test_func_name, param_names_str)
fixture_union_name = check_name_available(fixtures_dest, fixture_union_name, if_name_exists=CHANGE,
caller=parametrize)
# Retrieve (if ref) or create (for normal argvalues) the fixtures that we will union
fixture_alternatives = []
prev_i = -1
for i, j_list in fixture_indices: # noqa
# A/ Is there any non-empty group of 'normal' parameters before the fixture_ref at <i> ? If so, handle.
if i > prev_i + 1:
# create a new "param" fixture parametrized with all of that consecutive group.
# Important note: we could either wish to create one fixture for parameter value or to create
# one for each consecutive group as shown below. This should not lead to different results but perf
# might differ. Maybe add a parameter in the signature so that users can test it ?
# this would make the ids more readable by removing the "P2toP3"-like ids
p_fix_alt = _create_params_alt(fixtures_dest, test_func=test_func, hook=hook,
union_name=fixture_union_name, from_i=prev_i + 1, to_i=i)
fixture_alternatives.append(p_fix_alt)
# B/ Now handle the fixture ref at position <i>
if j_list is None:
# argvalues[i] contains a single argvalue that is a fixture_ref : add the referenced fixture
f_fix_alt = _create_fixture_ref_alt(union_name=fixture_union_name, test_func=test_func, i=i)
fixture_alternatives.append(f_fix_alt)
else:
# argvalues[i] is a tuple, some of them being fixture_ref. create a fixture refering to all of them
prod_fix_alt = _create_fixture_ref_product(fixtures_dest, union_name=fixture_union_name, i=i,
fixture_ref_positions=j_list,
test_func=test_func, hook=hook)
fixture_alternatives.append(prod_fix_alt)
prev_i = i
# C/ handle last consecutive group of normal parameters, if any
i = len(argvalues) # noqa
if i > prev_i + 1:
p_fix_alt = _create_params_alt(fixtures_dest, test_func=test_func, hook=hook,
union_name=fixture_union_name, from_i=prev_i + 1, to_i=i)
fixture_alternatives.append(p_fix_alt)
# if fixtures_to_union has length 1, simplify ? >> No, we leave such "optimization" to the end user
# Handle the list of alternative names. Duplicates should be removed here
fix_alt_names = []
for alt in fixture_alternatives:
if is_marked_parameter_value(alt):
# wrapped by a pytest.param
alt = get_marked_parameter_values(alt, nbargs=1)
assert len(alt) == 1, "Error with alternative please report"
alt = alt[0]
if alt.alternative_name not in fix_alt_names: