pytest_lazyfixture.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. # -*- coding: utf-8 -*-
  2. import copy
  3. import sys
  4. import types
  5. from collections import defaultdict
  6. import pytest
  7. PY3 = sys.version_info[0] == 3
  8. string_type = str if PY3 else basestring
  9. def pytest_configure():
  10. pytest.lazy_fixture = lazy_fixture
  11. @pytest.hookimpl(tryfirst=True)
  12. def pytest_runtest_setup(item):
  13. if hasattr(item, '_request'):
  14. item._request._fillfixtures = types.MethodType(
  15. fillfixtures(item._request._fillfixtures), item._request
  16. )
  17. def fillfixtures(_fillfixtures):
  18. def fill(request):
  19. item = request._pyfuncitem
  20. fixturenames = getattr(item, "fixturenames", None)
  21. if fixturenames is None:
  22. fixturenames = request.fixturenames
  23. if hasattr(item, 'callspec'):
  24. for param, val in sorted_by_dependency(item.callspec.params, fixturenames):
  25. if val is not None and is_lazy_fixture(val):
  26. item.callspec.params[param] = request.getfixturevalue(val.name)
  27. elif param not in item.funcargs:
  28. item.funcargs[param] = request.getfixturevalue(param)
  29. _fillfixtures()
  30. return fill
  31. @pytest.hookimpl(tryfirst=True)
  32. def pytest_fixture_setup(fixturedef, request):
  33. val = getattr(request, 'param', None)
  34. if is_lazy_fixture(val):
  35. request.param = request.getfixturevalue(val.name)
  36. def pytest_runtest_call(item):
  37. if hasattr(item, 'funcargs'):
  38. for arg, val in item.funcargs.items():
  39. if is_lazy_fixture(val):
  40. item.funcargs[arg] = item._request.getfixturevalue(val.name)
  41. @pytest.hookimpl(hookwrapper=True)
  42. def pytest_pycollect_makeitem(collector, name, obj):
  43. global current_node
  44. current_node = collector
  45. yield
  46. current_node = None
  47. def pytest_make_parametrize_id(config, val, argname):
  48. if is_lazy_fixture(val):
  49. return val.name
  50. @pytest.hookimpl(hookwrapper=True)
  51. def pytest_generate_tests(metafunc):
  52. yield
  53. normalize_metafunc_calls(metafunc, 'funcargs')
  54. normalize_metafunc_calls(metafunc, 'params')
  55. def normalize_metafunc_calls(metafunc, valtype, used_keys=None):
  56. newcalls = []
  57. for callspec in metafunc._calls:
  58. calls = normalize_call(callspec, metafunc, valtype, used_keys)
  59. newcalls.extend(calls)
  60. metafunc._calls = newcalls
  61. def copy_metafunc(metafunc):
  62. copied = copy.copy(metafunc)
  63. copied.fixturenames = copy.copy(metafunc.fixturenames)
  64. copied._calls = []
  65. try:
  66. copied._ids = copy.copy(metafunc._ids)
  67. except AttributeError:
  68. # pytest>=5.3.0
  69. pass
  70. copied._arg2fixturedefs = copy.copy(metafunc._arg2fixturedefs)
  71. return copied
  72. def normalize_call(callspec, metafunc, valtype, used_keys):
  73. fm = metafunc.config.pluginmanager.get_plugin('funcmanage')
  74. used_keys = used_keys or set()
  75. valtype_keys = set(getattr(callspec, valtype).keys()) - used_keys
  76. for arg in valtype_keys:
  77. val = getattr(callspec, valtype)[arg]
  78. if is_lazy_fixture(val):
  79. try:
  80. _, fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure([val.name], metafunc.definition.parent)
  81. except ValueError:
  82. # 3.6.0 <= pytest < 3.7.0; `FixtureManager.getfixtureclosure` returns 2 values
  83. fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure([val.name], metafunc.definition.parent)
  84. except AttributeError:
  85. # pytest < 3.6.0; `Metafunc` has no `definition` attribute
  86. fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure([val.name], current_node)
  87. extra_fixturenames = [fname for fname in fixturenames_closure
  88. if fname not in callspec.params and fname not in callspec.funcargs]
  89. newmetafunc = copy_metafunc(metafunc)
  90. newmetafunc.fixturenames = extra_fixturenames
  91. newmetafunc._arg2fixturedefs.update(arg2fixturedefs)
  92. newmetafunc._calls = [callspec]
  93. fm.pytest_generate_tests(newmetafunc)
  94. normalize_metafunc_calls(newmetafunc, valtype, used_keys | set([arg]))
  95. return newmetafunc._calls
  96. used_keys.add(arg)
  97. return [callspec]
  98. def sorted_by_dependency(params, fixturenames):
  99. free_fm = []
  100. non_free_fm = defaultdict(list)
  101. for key in _sorted_argnames(params, fixturenames):
  102. val = params.get(key)
  103. if key not in params or not is_lazy_fixture(val) or val.name not in params:
  104. free_fm.append(key)
  105. else:
  106. non_free_fm[val.name].append(key)
  107. non_free_fm_list = []
  108. for free_key in free_fm:
  109. non_free_fm_list.extend(
  110. _tree_to_list(non_free_fm, free_key)
  111. )
  112. return [(key, params.get(key)) for key in (free_fm + non_free_fm_list)]
  113. def _sorted_argnames(params, fixturenames):
  114. argnames = set(params.keys())
  115. for name in fixturenames:
  116. if name in argnames:
  117. argnames.remove(name)
  118. yield name
  119. if argnames:
  120. for name in argnames:
  121. yield name
  122. def _tree_to_list(trees, leave):
  123. lst = []
  124. for l in trees[leave]:
  125. lst.append(l)
  126. lst.extend(
  127. _tree_to_list(trees, l)
  128. )
  129. return lst
  130. def lazy_fixture(names):
  131. if isinstance(names, string_type):
  132. return LazyFixture(names)
  133. else:
  134. return [LazyFixture(name) for name in names]
  135. def is_lazy_fixture(val):
  136. return isinstance(val, LazyFixture)
  137. class LazyFixture(object):
  138. def __init__(self, name):
  139. self.name = name
  140. def __repr__(self):
  141. return '<{} "{}">'.format(self.__class__.__name__, self.name)
  142. def __eq__(self, other):
  143. return self.name == other.name