Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/mark/__init__.py: 54%

127 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

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

2from typing import AbstractSet 

3from typing import Collection 

4from typing import List 

5from typing import Optional 

6from typing import TYPE_CHECKING 

7from typing import Union 

8 

9import attr 

10 

11from .expression import Expression 

12from .expression import ParseError 

13from .structures import EMPTY_PARAMETERSET_OPTION 

14from .structures import get_empty_parameterset_mark 

15from .structures import Mark 

16from .structures import MARK_GEN 

17from .structures import MarkDecorator 

18from .structures import MarkGenerator 

19from .structures import ParameterSet 

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.stash import StashKey 

26 

27if 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 

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

42 

43 

44def 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 

71def 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) 

114def 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) 

134class 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 

184def 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) 

205class 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 

222def 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 

240def _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 

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

248 deselect_by_keyword(items, config) 

249 deselect_by_mark(items, config) 

250 

251 

252def 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 

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

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