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""" monkeypatching and mocking functionality. """ 

2import os 

3import re 

4import sys 

5import warnings 

6from contextlib import contextmanager 

7 

8import pytest 

9from _pytest.fixtures import fixture 

10from _pytest.pathlib import Path 

11 

12RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") 

13 

14 

15@fixture 

16def monkeypatch(): 

17 """The returned ``monkeypatch`` fixture provides these 

18 helper methods to modify objects, dictionaries or os.environ:: 

19 

20 monkeypatch.setattr(obj, name, value, raising=True) 

21 monkeypatch.delattr(obj, name, raising=True) 

22 monkeypatch.setitem(mapping, name, value) 

23 monkeypatch.delitem(obj, name, raising=True) 

24 monkeypatch.setenv(name, value, prepend=False) 

25 monkeypatch.delenv(name, raising=True) 

26 monkeypatch.syspath_prepend(path) 

27 monkeypatch.chdir(path) 

28 

29 All modifications will be undone after the requesting 

30 test function or fixture has finished. The ``raising`` 

31 parameter determines if a KeyError or AttributeError 

32 will be raised if the set/deletion operation has no target. 

33 """ 

34 mpatch = MonkeyPatch() 

35 yield mpatch 

36 mpatch.undo() 

37 

38 

39def resolve(name): 

40 # simplified from zope.dottedname 

41 parts = name.split(".") 

42 

43 used = parts.pop(0) 

44 found = __import__(used) 

45 for part in parts: 

46 used += "." + part 

47 try: 

48 found = getattr(found, part) 

49 except AttributeError: 

50 pass 

51 else: 

52 continue 

53 # we use explicit un-nesting of the handling block in order 

54 # to avoid nested exceptions on python 3 

55 try: 

56 __import__(used) 

57 except ImportError as ex: 

58 # str is used for py2 vs py3 

59 expected = str(ex).split()[-1] 

60 if expected == used: 

61 raise 

62 else: 

63 raise ImportError("import error in {}: {}".format(used, ex)) 

64 found = annotated_getattr(found, part, used) 

65 return found 

66 

67 

68def annotated_getattr(obj, name, ann): 

69 try: 

70 obj = getattr(obj, name) 

71 except AttributeError: 

72 raise AttributeError( 

73 "{!r} object at {} has no attribute {!r}".format( 

74 type(obj).__name__, ann, name 

75 ) 

76 ) 

77 return obj 

78 

79 

80def derive_importpath(import_path, raising): 

81 if not isinstance(import_path, str) or "." not in import_path: 

82 raise TypeError( 

83 "must be absolute import path string, not {!r}".format(import_path) 

84 ) 

85 module, attr = import_path.rsplit(".", 1) 

86 target = resolve(module) 

87 if raising: 

88 annotated_getattr(target, attr, ann=module) 

89 return attr, target 

90 

91 

92class Notset: 

93 def __repr__(self): 

94 return "<notset>" 

95 

96 

97notset = Notset() 

98 

99 

100class MonkeyPatch: 

101 """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. 

102 """ 

103 

104 def __init__(self): 

105 self._setattr = [] 

106 self._setitem = [] 

107 self._cwd = None 

108 self._savesyspath = None 

109 

110 @contextmanager 

111 def context(self): 

112 """ 

113 Context manager that returns a new :class:`MonkeyPatch` object which 

114 undoes any patching done inside the ``with`` block upon exit: 

115 

116 .. code-block:: python 

117 

118 import functools 

119 def test_partial(monkeypatch): 

120 with monkeypatch.context() as m: 

121 m.setattr(functools, "partial", 3) 

122 

123 Useful in situations where it is desired to undo some patches before the test ends, 

124 such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples 

125 of this see `#3290 <https://github.com/pytest-dev/pytest/issues/3290>`_. 

126 """ 

127 m = MonkeyPatch() 

128 try: 

129 yield m 

130 finally: 

131 m.undo() 

132 

133 def setattr(self, target, name, value=notset, raising=True): 

134 """ Set attribute value on target, memorizing the old value. 

135 By default raise AttributeError if the attribute did not exist. 

136 

137 For convenience you can specify a string as ``target`` which 

138 will be interpreted as a dotted import path, with the last part 

139 being the attribute name. Example: 

140 ``monkeypatch.setattr("os.getcwd", lambda: "/")`` 

141 would set the ``getcwd`` function of the ``os`` module. 

142 

143 The ``raising`` value determines if the setattr should fail 

144 if the attribute is not already present (defaults to True 

145 which means it will raise). 

146 """ 

147 __tracebackhide__ = True 

148 import inspect 

149 

150 if value is notset: 

151 if not isinstance(target, str): 

152 raise TypeError( 

153 "use setattr(target, name, value) or " 

154 "setattr(target, value) with target being a dotted " 

155 "import string" 

156 ) 

157 value = name 

158 name, target = derive_importpath(target, raising) 

159 

160 oldval = getattr(target, name, notset) 

161 if raising and oldval is notset: 

162 raise AttributeError("{!r} has no attribute {!r}".format(target, name)) 

163 

164 # avoid class descriptors like staticmethod/classmethod 

165 if inspect.isclass(target): 

166 oldval = target.__dict__.get(name, notset) 

167 self._setattr.append((target, name, oldval)) 

168 setattr(target, name, value) 

169 

170 def delattr(self, target, name=notset, raising=True): 

171 """ Delete attribute ``name`` from ``target``, by default raise 

172 AttributeError it the attribute did not previously exist. 

173 

174 If no ``name`` is specified and ``target`` is a string 

175 it will be interpreted as a dotted import path with the 

176 last part being the attribute name. 

177 

178 If ``raising`` is set to False, no exception will be raised if the 

179 attribute is missing. 

180 """ 

181 __tracebackhide__ = True 

182 import inspect 

183 

184 if name is notset: 

185 if not isinstance(target, str): 

186 raise TypeError( 

187 "use delattr(target, name) or " 

188 "delattr(target) with target being a dotted " 

189 "import string" 

190 ) 

191 name, target = derive_importpath(target, raising) 

192 

193 if not hasattr(target, name): 

194 if raising: 

195 raise AttributeError(name) 

196 else: 

197 oldval = getattr(target, name, notset) 

198 # Avoid class descriptors like staticmethod/classmethod. 

199 if inspect.isclass(target): 

200 oldval = target.__dict__.get(name, notset) 

201 self._setattr.append((target, name, oldval)) 

202 delattr(target, name) 

203 

204 def setitem(self, dic, name, value): 

205 """ Set dictionary entry ``name`` to value. """ 

206 self._setitem.append((dic, name, dic.get(name, notset))) 

207 dic[name] = value 

208 

209 def delitem(self, dic, name, raising=True): 

210 """ Delete ``name`` from dict. Raise KeyError if it doesn't exist. 

211 

212 If ``raising`` is set to False, no exception will be raised if the 

213 key is missing. 

214 """ 

215 if name not in dic: 

216 if raising: 

217 raise KeyError(name) 

218 else: 

219 self._setitem.append((dic, name, dic.get(name, notset))) 

220 del dic[name] 

221 

222 def setenv(self, name, value, prepend=None): 

223 """ Set environment variable ``name`` to ``value``. If ``prepend`` 

224 is a character, read the current environment variable value 

225 and prepend the ``value`` adjoined with the ``prepend`` character.""" 

226 if not isinstance(value, str): 

227 warnings.warn( 

228 pytest.PytestWarning( 

229 "Value of environment variable {name} type should be str, but got " 

230 "{value!r} (type: {type}); converted to str implicitly".format( 

231 name=name, value=value, type=type(value).__name__ 

232 ) 

233 ), 

234 stacklevel=2, 

235 ) 

236 value = str(value) 

237 if prepend and name in os.environ: 

238 value = value + prepend + os.environ[name] 

239 self.setitem(os.environ, name, value) 

240 

241 def delenv(self, name, raising=True): 

242 """ Delete ``name`` from the environment. Raise KeyError if it does 

243 not exist. 

244 

245 If ``raising`` is set to False, no exception will be raised if the 

246 environment variable is missing. 

247 """ 

248 self.delitem(os.environ, name, raising=raising) 

249 

250 def syspath_prepend(self, path): 

251 """ Prepend ``path`` to ``sys.path`` list of import locations. """ 

252 from pkg_resources import fixup_namespace_packages 

253 

254 if self._savesyspath is None: 

255 self._savesyspath = sys.path[:] 

256 sys.path.insert(0, str(path)) 

257 

258 # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171 

259 fixup_namespace_packages(str(path)) 

260 

261 # A call to syspathinsert() usually means that the caller wants to 

262 # import some dynamically created files, thus with python3 we 

263 # invalidate its import caches. 

264 # This is especially important when any namespace package is in used, 

265 # since then the mtime based FileFinder cache (that gets created in 

266 # this case already) gets not invalidated when writing the new files 

267 # quickly afterwards. 

268 from importlib import invalidate_caches 

269 

270 invalidate_caches() 

271 

272 def chdir(self, path): 

273 """ Change the current working directory to the specified path. 

274 Path can be a string or a py.path.local object. 

275 """ 

276 if self._cwd is None: 

277 self._cwd = os.getcwd() 

278 if hasattr(path, "chdir"): 

279 path.chdir() 

280 elif isinstance(path, Path): 

281 # modern python uses the fspath protocol here LEGACY 

282 os.chdir(str(path)) 

283 else: 

284 os.chdir(path) 

285 

286 def undo(self): 

287 """ Undo previous changes. This call consumes the 

288 undo stack. Calling it a second time has no effect unless 

289 you do more monkeypatching after the undo call. 

290 

291 There is generally no need to call `undo()`, since it is 

292 called automatically during tear-down. 

293 

294 Note that the same `monkeypatch` fixture is used across a 

295 single test function invocation. If `monkeypatch` is used both by 

296 the test function itself and one of the test fixtures, 

297 calling `undo()` will undo all of the changes made in 

298 both functions. 

299 """ 

300 for obj, name, value in reversed(self._setattr): 

301 if value is not notset: 

302 setattr(obj, name, value) 

303 else: 

304 delattr(obj, name) 

305 self._setattr[:] = [] 

306 for dictionary, name, value in reversed(self._setitem): 

307 if value is notset: 

308 try: 

309 del dictionary[name] 

310 except KeyError: 

311 pass # was already deleted, so we have the desired state 

312 else: 

313 dictionary[name] = value 

314 self._setitem[:] = [] 

315 if self._savesyspath is not None: 

316 sys.path[:] = self._savesyspath 

317 self._savesyspath = None 

318 

319 if self._cwd is not None: 

320 os.chdir(self._cwd) 

321 self._cwd = None