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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

""" monkeypatching and mocking functionality.  """ 

 

import os, sys 

import re 

 

from py.builtin import _basestring 

 

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

 

 

def pytest_funcarg__monkeypatch(request): 

    """The returned ``monkeypatch`` funcarg provides these 

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

 

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

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

        monkeypatch.setitem(mapping, name, value) 

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

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

        monkeypatch.delenv(name, value, raising=True) 

        monkeypatch.syspath_prepend(path) 

        monkeypatch.chdir(path) 

 

    All modifications will be undone after the requesting 

    test function has finished. The ``raising`` 

    parameter determines if a KeyError or AttributeError 

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

    """ 

    mpatch = monkeypatch() 

    request.addfinalizer(mpatch.undo) 

    return mpatch 

 

 

def resolve(name): 

    # simplified from zope.dottedname 

    parts = name.split('.') 

 

    used = parts.pop(0) 

    found = __import__(used) 

    for part in parts: 

        used += '.' + part 

        try: 

            found = getattr(found, part) 

        except AttributeError: 

            pass 

        else: 

            continue 

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

        # to avoid nested exceptions on python 3 

        try: 

            __import__(used) 

        except ImportError as ex: 

            # str is used for py2 vs py3 

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

            if expected == used: 

                raise 

            else: 

                raise ImportError( 

                    'import error in %s: %s' % (used, ex) 

                ) 

        found = annotated_getattr(found, part, used) 

    return found 

 

 

def annotated_getattr(obj, name, ann): 

    try: 

        obj = getattr(obj, name) 

    except AttributeError: 

        raise AttributeError( 

                '%r object at %s has no attribute %r' % ( 

                    type(obj).__name__, ann, name 

                ) 

        ) 

    return obj 

 

 

def derive_importpath(import_path, raising): 

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

        raise TypeError("must be absolute import path string, not %r" % 

                        (import_path,)) 

    module, attr = import_path.rsplit('.', 1) 

    target = resolve(module) 

    if raising: 

        annotated_getattr(target, attr, ann=module) 

    return attr, target 

 

 

class Notset: 

    def __repr__(self): 

        return "<notset>" 

 

 

notset = Notset() 

 

 

class monkeypatch: 

    """ Object keeping a record of setattr/item/env/syspath changes. """ 

 

    def __init__(self): 

        self._setattr = [] 

        self._setitem = [] 

        self._cwd = None 

        self._savesyspath = None 

 

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

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

        By default raise AttributeError if the attribute did not exist. 

 

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

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

        being the attribute name.  Example: 

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

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

 

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

        if the attribute is not already present (defaults to True 

        which means it will raise). 

        """ 

        __tracebackhide__ = True 

        import inspect 

 

123        if value is notset: 

            if not isinstance(target, _basestring): 

                raise TypeError("use setattr(target, name, value) or " 

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

                                "import string") 

            value = name 

            name, target = derive_importpath(target, raising) 

 

        oldval = getattr(target, name, notset) 

132        if raising and oldval is notset: 

            raise AttributeError("%r has no attribute %r" % (target, name)) 

 

        # avoid class descriptors like staticmethod/classmethod 

136        if inspect.isclass(target): 

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

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

        setattr(target, name, value) 

 

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

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

        AttributeError it the attribute did not previously exist. 

 

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

        it will be interpreted as a dotted import path with the 

        last part being the attribute name. 

 

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

        attribute is missing. 

        """ 

        __tracebackhide__ = True 

        if name is notset: 

            if not isinstance(target, _basestring): 

                raise TypeError("use delattr(target, name) or " 

                                "delattr(target) with target being a dotted " 

                                "import string") 

            name, target = derive_importpath(target, raising) 

 

        if not hasattr(target, name): 

            if raising: 

                raise AttributeError(name) 

        else: 

            self._setattr.append((target, name, getattr(target, name, notset))) 

            delattr(target, name) 

 

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

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

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

        dic[name] = value 

 

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

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

 

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

        key is missing. 

        """ 

        if name not in dic: 

            if raising: 

                raise KeyError(name) 

        else: 

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

            del dic[name] 

 

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

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

        is a character, read the current environment variable value 

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

        value = str(value) 

        if prepend and name in os.environ: 

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

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

 

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

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

        exist. 

 

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

        environment variable is missing. 

        """ 

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

 

    def syspath_prepend(self, path): 

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

        if self._savesyspath is None: 

            self._savesyspath = sys.path[:] 

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

 

    def chdir(self, path): 

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

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

        """ 

        if self._cwd is None: 

            self._cwd = os.getcwd() 

        if hasattr(path, "chdir"): 

            path.chdir() 

        else: 

            os.chdir(path) 

 

    def undo(self): 

        """ Undo previous changes.  This call consumes the 

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

        you do more monkeypatching after the undo call. 

         

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

        called automatically during tear-down. 

         

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

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

        the test function itself and one of the test fixtures, 

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

        both functions. 

        """ 

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

            if value is not notset: 

                setattr(obj, name, value) 

            else: 

                delattr(obj, name) 

        self._setattr[:] = [] 

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

            if value is notset: 

                try: 

                    del dictionary[name] 

                except KeyError: 

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

            else: 

                dictionary[name] = value 

        self._setitem[:] = [] 

249        if self._savesyspath is not None: 

            sys.path[:] = self._savesyspath 

            self._savesyspath = None 

 

253        if self._cwd is not None: 

            os.chdir(self._cwd) 

            self._cwd = None