Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

2import typing 

3from typing import AbstractSet 

4from typing import List 

5from typing import Optional 

6from typing import Union 

7 

8import attr 

9 

10from .expression import Expression 

11from .expression import ParseError 

12from .structures import EMPTY_PARAMETERSET_OPTION 

13from .structures import get_empty_parameterset_mark 

14from .structures import Mark 

15from .structures import MARK_GEN 

16from .structures import MarkDecorator 

17from .structures import MarkGenerator 

18from .structures import ParameterSet 

19from _pytest.compat import TYPE_CHECKING 

20from _pytest.config import Config 

21from _pytest.config import ExitCode 

22from _pytest.config import hookimpl 

23from _pytest.config import UsageError 

24from _pytest.config.argparsing import Parser 

25from _pytest.store import StoreKey 

26 

27if TYPE_CHECKING: 

28 from _pytest.nodes import Item 

29 

30 

31__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"] 

32 

33 

34old_mark_config_key = StoreKey[Optional[Config]]() 

35 

36 

37def param( 

38 *values: object, 

39 marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (), 

40 id: Optional[str] = None 

41) -> ParameterSet: 

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

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

44 

45 .. code-block:: python 

46 

47 @pytest.mark.parametrize( 

48 "test_input,expected", 

49 [("3+5", 8), pytest.param("6*9", 42, marks=pytest.mark.xfail),], 

50 ) 

51 def test_eval(test_input, expected): 

52 assert eval(test_input) == expected 

53 

54 :param values: variable args of the values of the parameter set, in order. 

55 :keyword marks: a single mark or a list of marks to be applied to this parameter set. 

56 :keyword str id: the id to attribute to this parameter set. 

57 """ 

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

59 

60 

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

62 group = parser.getgroup("general") 

63 group._addoption( 

64 "-k", 

65 action="store", 

66 dest="keyword", 

67 default="", 

68 metavar="EXPRESSION", 

69 help="only run tests which match the given substring expression. " 

70 "An expression is a python evaluatable expression " 

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

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

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

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

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

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

77 "Additionally keywords are matched to classes and functions " 

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

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

80 "The matching is case-insensitive.", 

81 ) 

82 

83 group._addoption( 

84 "-m", 

85 action="store", 

86 dest="markexpr", 

87 default="", 

88 metavar="MARKEXPR", 

89 help="only run tests matching given mark expression.\n" 

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

91 ) 

92 

93 group.addoption( 

94 "--markers", 

95 action="store_true", 

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

97 ) 

98 

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

100 parser.addini(EMPTY_PARAMETERSET_OPTION, "default marker for empty parametersets") 

101 

102 

103@hookimpl(tryfirst=True) 

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

105 import _pytest.config 

106 

107 if config.option.markers: 

108 config._do_configure() 

109 tw = _pytest.config.create_terminal_writer(config) 

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

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

112 name = parts[0] 

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

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

115 tw.line(rest) 

116 tw.line() 

117 config._ensure_unconfigure() 

118 return 0 

119 

120 return None 

121 

122 

123@attr.s(slots=True) 

124class KeywordMatcher: 

125 """A matcher for keywords. 

126 

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

128 string inclusion check is case-insensitive. 

129 

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

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

132 :class:`Function`. 

133 

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

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

136 """ 

137 

138 _names = attr.ib(type=AbstractSet[str]) 

139 

140 @classmethod 

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

142 mapped_names = set() 

143 

144 # Add the names of the current item and any parent items 

145 import pytest 

146 

147 for node in item.listchain(): 

148 if not isinstance(node, (pytest.Instance, pytest.Session)): 

149 mapped_names.add(node.name) 

150 

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

152 mapped_names.update(item.listextrakeywords()) 

153 

154 # Add the names attached to the current function through direct assignment 

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

156 if function_obj: 

157 mapped_names.update(function_obj.__dict__) 

158 

159 # add the markers to the keywords as we no longer handle them correctly 

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

161 

162 return cls(mapped_names) 

163 

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

165 subname = subname.lower() 

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

167 

168 for name in names: 

169 if subname in name: 

170 return True 

171 return False 

172 

173 

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

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

176 if not keywordexpr: 

177 return 

178 

179 if keywordexpr.startswith("-"): 

180 # To be removed in pytest 7.0.0. 

181 # Uncomment this after 6.0 release (#7361) 

182 # warnings.warn(MINUS_K_DASH, stacklevel=2) 

183 keywordexpr = "not " + keywordexpr[1:] 

184 selectuntil = False 

185 if keywordexpr[-1:] == ":": 

186 # To be removed in pytest 7.0.0. 

187 # Uncomment this after 6.0 release (#7361) 

188 # warnings.warn(MINUS_K_COLON, stacklevel=2) 

189 selectuntil = True 

190 keywordexpr = keywordexpr[:-1] 

191 

192 try: 

193 expression = Expression.compile(keywordexpr) 

194 except ParseError as e: 

195 raise UsageError( 

196 "Wrong expression passed to '-k': {}: {}".format(keywordexpr, e) 

197 ) from None 

198 

199 remaining = [] 

200 deselected = [] 

201 for colitem in items: 

202 if keywordexpr and not expression.evaluate(KeywordMatcher.from_item(colitem)): 

203 deselected.append(colitem) 

204 else: 

205 if selectuntil: 

206 keywordexpr = None 

207 remaining.append(colitem) 

208 

209 if deselected: 

210 config.hook.pytest_deselected(items=deselected) 

211 items[:] = remaining 

212 

213 

214@attr.s(slots=True) 

215class MarkMatcher: 

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

217 

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

219 """ 

220 

221 own_mark_names = attr.ib() 

222 

223 @classmethod 

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

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

226 return cls(mark_names) 

227 

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

229 return name in self.own_mark_names 

230 

231 

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

233 matchexpr = config.option.markexpr 

234 if not matchexpr: 

235 return 

236 

237 try: 

238 expression = Expression.compile(matchexpr) 

239 except ParseError as e: 

240 raise UsageError( 

241 "Wrong expression passed to '-m': {}: {}".format(matchexpr, e) 

242 ) from None 

243 

244 remaining = [] 

245 deselected = [] 

246 for item in items: 

247 if expression.evaluate(MarkMatcher.from_item(item)): 

248 remaining.append(item) 

249 else: 

250 deselected.append(item) 

251 

252 if deselected: 

253 config.hook.pytest_deselected(items=deselected) 

254 items[:] = remaining 

255 

256 

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

258 deselect_by_keyword(items, config) 

259 deselect_by_mark(items, config) 

260 

261 

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

263 config._store[old_mark_config_key] = MARK_GEN._config 

264 MARK_GEN._config = config 

265 

266 empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) 

267 

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

269 raise UsageError( 

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

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

272 ) 

273 

274 

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

276 MARK_GEN._config = config._store.get(old_mark_config_key, None)