1"""Generic mechanism for marking and selecting python functions."""

2 from typing import AbstractSet

3 from typing import Collection

4 from typing import List

5 from typing import Optional

6 from typing import TYPE_CHECKING

7 from typing import Union

8

9 import attr

10

11 from .expression import Expression

12 from .expression import ParseError

13 from .structures import EMPTY_PARAMETERSET_OPTION

14 from .structures import get_empty_parameterset_mark

15 from .structures import Mark

16 from .structures import MARK_GEN

17 from .structures import MarkDecorator

18 from .structures import MarkGenerator

19 from .structures import ParameterSet

20 from _pytest.config import Config

21 from _pytest.config import ExitCode

22 from _pytest.config import hookimpl

23 from _pytest.config import UsageError

24 from _pytest.config.argparsing import Parser

25 from _pytest.stash import StashKey

26

27 if TYPE_CHECKING:

28 from _pytest.nodes import Item

29

30

31__all__ = [

32 "MARK_GEN",

33 "Mark",

34 "MarkDecorator",

35 "MarkGenerator",

36 "ParameterSet",

37 "get_empty_parameterset_mark",

38]

39

40

41 old_mark_config_key = StashKey[Optional[Config]]()

42

43

44 def param(

45 *values: object,

46 marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),

47 id: Optional[str] = None,

48) -> ParameterSet:

49 """Specify a parameter in `pytest.mark.parametrize`_ calls or

50 :ref:`parametrized fixtures <fixture-parametrize-marks>`.

51

52 .. code-block:: python

53

54 @pytest.mark.parametrize(

55 "test_input,expected",

56 [

57 ("3+5", 8),

58 pytest.param("6*9", 42, marks=pytest.mark.xfail),

59 ],

60 )

61 def test_eval(test_input, expected):

62 assert eval(test_input) == expected

63

64 :param values: Variable args of the values of the parameter set, in order.

65 :param marks: A single mark or a list of marks to be applied to this parameter set.

66 :param id: The id to attribute to this parameter set.

67 """

68 return ParameterSet.param(*values, marks=marks, id=id)

69

70

71 def pytest_addoption(parser: Parser) -> None:

72 group = parser.getgroup("general")

73 group._addoption(

74 "-k",

75 action="store",

76 dest="keyword",

77 default="",

78 metavar="EXPRESSION",

79 help="Only run tests which match the given substring expression. "

80 "An expression is a Python evaluatable expression "

81 "where all names are substring-matched against test names "

82 "and their parent classes. Example: -k 'test_method or test_"

83 "other' matches all test functions and classes whose name "

84 "contains 'test_method' or 'test_other', while -k 'not test_method' "

85 "matches those that don't contain 'test_method' in their names. "

86 "-k 'not test_method and not test_other' will eliminate the matches. "

87 "Additionally keywords are matched to classes and functions "

88 "containing extra names in their 'extra_keyword_matches' set, "

89 "as well as functions which have names assigned directly to them. "

90 "The matching is case-insensitive.",

91 )

92

93 group._addoption(

94 "-m",

95 action="store",

96 dest="markexpr",

97 default="",

98 metavar="MARKEXPR",

99 help="Only run tests matching given mark expression. "

100 "For example: -m 'mark1 and not mark2'.",

101 )

102

103 group.addoption(

104 "--markers",

105 action="store_true",

106 help="show markers (builtin, plugin and per-project ones).",

107 )

108

109 parser.addini("markers", "Markers for test functions", "linelist")

110 parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets")

111

112

113@hookimpl(tryfirst=True)

114 def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:

115 import _pytest.config

116

117 if config.option.markers:

118 config._do_configure()

119 tw = _pytest.config.create_terminal_writer(config)

120 for line in config.getini("markers"):

121 parts = line.split(":", 1)

122 name = parts[0]

123 rest = parts[1] if len(parts) == 2 else ""

124 tw.write("@pytest.mark.%s:" % name, bold=True)

125 tw.line(rest)

126 tw.line()

127 config._ensure_unconfigure()

128 return 0

129

130 return None

131

132

133@attr.s(slots=True, auto_attribs=True)

134 class KeywordMatcher:

135 """A matcher for keywords.

136

137 Given a list of names, matches any substring of one of these names. The

138 string inclusion check is case-insensitive.

139

140 Will match on the name of colitem, including the names of its parents.

141 Only matches names of items which are either a :class:`Class` or a

142 :class:`Function`.

143

144 Additionally, matches on names in the 'extra_keyword_matches' set of

145 any item, as well as names directly assigned to test functions.

146 """

147

148 _names: AbstractSet[str]

149

150 @classmethod

151 def from_item(cls, item: "Item") -> "KeywordMatcher":

152 mapped_names = set()

153

154 # Add the names of the current item and any parent items.

155 import pytest

156

157 for node in item.listchain():

158 if not isinstance(node, pytest.Session):

159 mapped_names.add(node.name)

160

161 # Add the names added as extra keywords to current or parent items.

162 mapped_names.update(item.listextrakeywords())

163

164 # Add the names attached to the current function through direct assignment.

165 function_obj = getattr(item, "function", None)

166 if function_obj:

167 mapped_names.update(function_obj.__dict__)

168

169 # Add the markers to the keywords as we no longer handle them correctly.

170 mapped_names.update(mark.name for mark in item.iter_markers())

171

172 return cls(mapped_names)

173

174 def __call__(self, subname: str) -> bool:

175 subname = subname.lower()

176 names = (name.lower() for name in self._names)

177

178 for name in names:

179 if subname in name:

180 return True

181 return False

182

183

184 def deselect_by_keyword(items: "List[Item]", config: Config) -> None:

185 keywordexpr = config.option.keyword.lstrip()

186 if not keywordexpr:

187 return

188

189 expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'")

190

191 remaining = []

192 deselected = []

193 for colitem in items:

194 if not expr.evaluate(KeywordMatcher.from_item(colitem)):

195 deselected.append(colitem)

196 else:

197 remaining.append(colitem)

198

199 if deselected:

200 config.hook.pytest_deselected(items=deselected)

201 items[:] = remaining

202

203

204@attr.s(slots=True, auto_attribs=True)

205 class MarkMatcher:

206 """A matcher for markers which are present.

207

208 Tries to match on any marker names, attached to the given colitem.

209 """

210

211 own_mark_names: AbstractSet[str]

212

213 @classmethod

214 def from_item(cls, item: "Item") -> "MarkMatcher":

215 mark_names = {mark.name for mark in item.iter_markers()}

216 return cls(mark_names)

217

218 def __call__(self, name: str) -> bool:

219 return name in self.own_mark_names

220

221

222 def deselect_by_mark(items: "List[Item]", config: Config) -> None:

223 matchexpr = config.option.markexpr

224 if not matchexpr:

225 return

226

227 expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'")

228 remaining: List[Item] = []

229 deselected: List[Item] = []

230 for item in items:

231 if expr.evaluate(MarkMatcher.from_item(item)):

232 remaining.append(item)

233 else:

234 deselected.append(item)

235 if deselected:

236 config.hook.pytest_deselected(items=deselected)

237 items[:] = remaining

238

239

240 def _parse_expression(expr: str, exc_message: str) -> Expression:

241 try:

242 return Expression.compile(expr)

243 except ParseError as e:

244 raise UsageError(f"{exc_message}: {expr}: {e}") from None

245

246

247 def pytest_collection_modifyitems(items: "List[Item]", config: Config) -> None:

248 deselect_by_keyword(items, config)

249 deselect_by_mark(items, config)

250

251

252 def pytest_configure(config: Config) -> None:

253 config.stash[old_mark_config_key] = MARK_GEN._config

254 MARK_GEN._config = config

255

256 empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)

257

258 if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):

259 raise UsageError(

260 "{!s} must be one of skip, xfail or fail_at_collect"

261 " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset)

262 )

263

264

265 def pytest_unconfigure(config: Config) -> None:

266 MARK_GEN._config = config.stash.get(old_mark_config_key, None)