Skip to content

Commit e3739e5

Browse files
author
Sylvain MARIE
committed
First version working with all pytest versions on windows. Ready to try with CI.
1 parent e6e4508 commit e3739e5

File tree

4 files changed

+116
-38
lines changed

4 files changed

+116
-38
lines changed

pytest_cases/common.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,27 @@ def get_pytest_marks_on_function(f):
9292
return []
9393

9494

95+
def is_function_node(node):
96+
try:
97+
node.function
98+
return True
99+
except AttributeError:
100+
return False
101+
102+
103+
def get_parametrization_markers(node):
104+
"""
105+
Returns the parametrization marks on a function node.
106+
Not sure how different this one is from the ones below, probably the input is different
107+
:param node:
108+
:return:
109+
"""
110+
if LooseVersion(pytest.__version__) >= LooseVersion('3.4.0'):
111+
return list(node.iter_markers(name="parametrize"))
112+
else:
113+
return list(node.parametrize)
114+
115+
95116
def get_pytest_parametrize_marks(f):
96117
"""
97118
Returns the @pytest.mark.parametrize marks associated with a function (and only those)
@@ -300,3 +321,20 @@ def get_pytest_nodeid(metafunc):
300321
return metafunc.definition.nodeid
301322
except AttributeError:
302323
return "unknown"
324+
325+
326+
def get_pytest_scopes():
327+
"""
328+
Returns the list of scopes in order as defined in pytest.
329+
:return:
330+
"""
331+
try:
332+
from _pytest.fixtures import scopes as pt_scopes
333+
except ImportError:
334+
# pytest 2
335+
from _pytest.python import scopes as pt_scopes
336+
return pt_scopes
337+
338+
339+
def get_pytest_function_scopenum():
340+
return get_pytest_scopes().index("function")

pytest_cases/main_fixtures.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,8 +628,8 @@ def _new_fixture(request, **all_fixtures):
628628

629629
# finally create the fixture per se.
630630
# WARNING we do not use pytest.fixture but pytest_fixture_plus so that NOT_USED is discarded
631-
f_decorator = pytest_fixture_plus(scope=scope, params=[UnionFixtureAlternative(name) for name in f_names], autouse=autouse,
632-
ids=ids, **kwargs)
631+
f_decorator = pytest_fixture_plus(scope=scope, params=[UnionFixtureAlternative(_name) for _name in f_names],
632+
autouse=autouse, ids=ids, **kwargs)
633633
fix = f_decorator(_new_fixture)
634634

635635
# Dynamically add fixture to caller's module as explained in https://github.com/pytest-dev/pytest/issues/2424

pytest_cases/plugin.py

Lines changed: 74 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from collections import OrderedDict, namedtuple
2+
from copy import copy
23
from distutils.version import LooseVersion
3-
from functools import partial
4+
from warnings import warn
45

5-
from _pytest.fixtures import scopes as pt_scopes
6+
from functools import partial
67

78
import pytest
89

9-
from pytest_cases.common import get_pytest_nodeid
10+
from pytest_cases.common import get_pytest_nodeid, get_parametrization_markers, get_pytest_function_scopenum, \
11+
is_function_node
1012
from pytest_cases.main_fixtures import NOT_USED, is_fixture_union_params
1113

1214
try: # python 3.3+
@@ -22,7 +24,7 @@
2224
pass
2325

2426

25-
_DEBUG = True
27+
_DEBUG = False
2628

2729

2830
# @hookspec(firstresult=True)
@@ -114,6 +116,14 @@ def __iter__(self):
114116
def __getitem__(self, item):
115117
return self.to_list()[item]
116118

119+
def __setitem__(self, key, value):
120+
# This is called in Pytest 4+. TODO how should we behave ?
121+
warn("WARNING the new order is not taken into account !!")
122+
pass
123+
124+
def index(self, *args):
125+
return self.to_list().index(*args)
126+
117127
def to_list(self):
118128
"""
119129
Converts self to a list to get all fixture names, and caches the result.
@@ -135,7 +145,7 @@ def sort_by_scope(arg_name):
135145
try:
136146
fixturedefs = self.get_all_fixture_defs()[arg_name]
137147
except KeyError:
138-
return pt_scopes.index("function")
148+
return get_pytest_function_scopenum()
139149
else:
140150
return fixturedefs[-1].scopenum
141151
fixturenames_closure.sort(key=sort_by_scope)
@@ -319,7 +329,7 @@ def split_and_build(self,
319329

320330
# perform the propagation:
321331
# create a copy of the pending fixtures list and prepend the fixture used
322-
pending_for_child = pending_fixtures_list.copy()
332+
pending_for_child = copy(pending_fixtures_list)
323333
# (a) first propagate all child's dependencies
324334
new_c._build_closure(fixture_defs_mgr, [f])
325335
# (b) then the ones required by parent
@@ -383,6 +393,12 @@ def get_alternatives(self):
383393

384394

385395
def merge(new_items, into_list):
396+
"""
397+
Appends items from `new_items` into `into_list`, only if they are not already there.
398+
:param new_items:
399+
:param into_list:
400+
:return:
401+
"""
386402
at_least_one_added = False
387403
for l in new_items:
388404
if l not in into_list:
@@ -395,7 +411,7 @@ def getfixtureclosure(fm, fixturenames, parentnode):
395411

396412
# first retrieve the normal pytest output for comparison
397413
outputs = fm.__class__.getfixtureclosure(fm, fixturenames, parentnode)
398-
if LooseVersion(pytest.__version__) >= LooseVersion('4.0.0'):
414+
if LooseVersion(pytest.__version__) >= LooseVersion('3.10.0'):
399415
initial_names, ref_fixturenames, ref_arg2fixturedefs = outputs
400416
else:
401417
ref_fixturenames, ref_arg2fixturedefs = outputs
@@ -409,18 +425,23 @@ def getfixtureclosure(fm, fixturenames, parentnode):
409425

410426
# -- required fixtures/params.
411427
# ********* fix the order of initial fixtures: indeed this order may not be the right one ************
412-
p_markers = list(parentnode.iter_markers(name="parametrize"))
413-
cur_indices = []
414-
for paramz_mark in p_markers:
415-
param_names = paramz_mark.args[0].replace(' ', '').split(',')
416-
for pname in param_names:
417-
cur_indices.append(fixturenames.index(pname))
418-
target_indices = sorted(cur_indices)
419-
sorted_fixturenames = list(fixturenames)
420-
for old_i, new_i in zip(cur_indices, target_indices):
421-
sorted_fixturenames[new_i] = fixturenames[old_i]
422-
# **********
423-
merge(sorted_fixturenames, _init_fixnames)
428+
# this only works when pytest version is > 3.4, otherwise the parent node is a Module
429+
if is_function_node(parentnode):
430+
p_markers = get_parametrization_markers(parentnode)
431+
cur_indices = []
432+
for paramz_mark in p_markers:
433+
param_names = paramz_mark.args[0].replace(' ', '').split(',')
434+
for pname in param_names:
435+
cur_indices.append(fixturenames.index(pname))
436+
target_indices = sorted(cur_indices)
437+
sorted_fixturenames = list(fixturenames)
438+
for old_i, new_i in zip(cur_indices, target_indices):
439+
sorted_fixturenames[new_i] = fixturenames[old_i]
440+
# **********
441+
merge(sorted_fixturenames, _init_fixnames)
442+
else:
443+
# we cannot sort yet
444+
merge(fixturenames, _init_fixnames)
424445

425446
# Finally create the closure tree
426447
fixture_defs_mger = FixtureDefsCache(fm, parentid)
@@ -453,8 +474,9 @@ def getfixtureclosure(fm, fixturenames, parentnode):
453474
# store_union_closure_in_node(fixturenames_closure_node, parentnode)
454475

455476
# return ref_fixturenames, ref_arg2fixturedefs
456-
if LooseVersion(pytest.__version__) >= LooseVersion('4.0.0'):
457-
return initial_names, fixturenames_closure_node, ref_arg2fixturedefs
477+
if LooseVersion(pytest.__version__) >= LooseVersion('3.10.0'):
478+
our_initial_names = sorted_fixturenames # initial_names
479+
return our_initial_names, fixturenames_closure_node, ref_arg2fixturedefs
458480
else:
459481
return fixturenames_closure_node, ref_arg2fixturedefs
460482

@@ -638,11 +660,39 @@ def create_call_list_from_pending_parametrizations(self):
638660

639661
calls, nodes = self._process_node(fix_closure_tree, pending.copy(), [])
640662

663+
self._cleanup_calls_list(calls, nodes, pending)
664+
665+
if _DEBUG:
666+
print("\n".join(["%s[%s]: funcargs=%s, params=%s" % (get_pytest_nodeid(self.metafunc),
667+
c.id, c.funcargs, c.params)
668+
for c in calls]))
669+
print()
670+
671+
self._call_list = calls
672+
673+
# put back self as the _calls facade
674+
self.metafunc._calls = bak_calls
675+
676+
# forget about all parametrizations now - this wont happen again
677+
self._pending = None
678+
679+
def _cleanup_calls_list(self, calls, nodes, pending):
680+
"""
681+
Cleans the calls list so that all calls contain a value for all parameters. This is basically
682+
about adding "NOT_USED" parametrization everywhere relevant.
683+
684+
:param calls:
685+
:param nodes:
686+
:param pending:
687+
:return:
688+
"""
689+
641690
nb_calls = len(calls)
642691
if nb_calls != len(nodes):
643692
raise ValueError("This should not happen !")
644693

645-
# Cleanup:
694+
function_scope_num = get_pytest_function_scopenum()
695+
646696
for i in range(nb_calls):
647697
c, n = calls[i], nodes[i]
648698

@@ -678,20 +728,8 @@ def create_call_list_from_pending_parametrizations(self):
678728
if fixture not in c.params and fixture not in c.funcargs:
679729
# explicitly add it as discarded by creating a parameter value for it.
680730
c.params[fixture] = NOT_USED
681-
682-
if _DEBUG:
683-
print("\n".join(["%s[%s]: funcargs=%s, params=%s" % (get_pytest_nodeid(self.metafunc),
684-
c.id, c.funcargs, c.params)
685-
for c in calls]))
686-
print()
687-
688-
self._call_list = calls
689-
690-
# put back self as the _calls facade
691-
self.metafunc._calls = bak_calls
692-
693-
# forget about all parametrizations now - this wont happen again
694-
self._pending = None
731+
c.indices[fixture] = 0
732+
c._arg2scopenum[fixture] = function_scope_num
695733

696734
def _parametrize_calls(self, init_calls, argnames, argvalues, discard_id=False, indirect=False, ids=None,
697735
scope=None, **kwargs):

pytest_cases/tests/fixtures/test_fixtures_parametrize_stereo.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def test_stereo_two_parametrizers(stereo_cfg):
6666
b = StateAsserter()
6767

6868

69+
@pytest.mark.skipif(LooseVersion(pytest.__version__) < LooseVersion('3.4.0'),
70+
reason="with old versions of pytest pytest-cases cannot fix the parametrization order.")
6971
@pytest.mark.parametrize("path", STEREO_PATHS)
7072
@pytest.mark.parametrize("cfg_factory", CFG_TYPES) # not actual params
7173
def test_reference_test(path, cfg_factory, request):

0 commit comments

Comments
 (0)