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

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

"""Utilities for assertion debugging""" 

import pprint 

 

import _pytest._code 

import py 

try: 

    from collections import Sequence 

except ImportError: 

    Sequence = list 

 

BuiltinAssertionError = py.builtin.builtins.AssertionError 

u = py.builtin._totext 

 

# The _reprcompare attribute on the util module is used by the new assertion 

# interpretation code and assertion rewriter to detect this plugin was 

# loaded and in turn call the hooks defined here as part of the 

# DebugInterpreter. 

_reprcompare = None 

 

 

# the re-encoding is needed for python2 repr 

# with non-ascii characters (see issue 877 and 1379) 

def ecu(s): 

    try: 

        return u(s, 'utf-8', 'replace') 

    except TypeError: 

        return s 

 

 

def format_explanation(explanation): 

    """This formats an explanation 

 

    Normally all embedded newlines are escaped, however there are 

    three exceptions: \n{, \n} and \n~.  The first two are intended 

    cover nested explanations, see function and attribute explanations 

    for examples (.visit_Call(), visit_Attribute()).  The last one is 

    for when one explanation needs to span multiple lines, e.g. when 

    displaying diffs. 

    """ 

    explanation = ecu(explanation) 

    explanation = _collapse_false(explanation) 

    lines = _split_explanation(explanation) 

    result = _format_lines(lines) 

    return u('\n').join(result) 

 

 

def _collapse_false(explanation): 

    """Collapse expansions of False 

 

    So this strips out any "assert False\n{where False = ...\n}" 

    blocks. 

    """ 

    where = 0 

    while True: 

        start = where = explanation.find("False\n{False = ", where) 

58        if where == -1: 

            break 

        level = 0 

        prev_c = explanation[start] 

        for i, c in enumerate(explanation[start:]): 

            if prev_c + c == "\n{": 

                level += 1 

            elif prev_c + c == "\n}": 

                level -= 1 

                if not level: 

                    break 

            prev_c = c 

        else: 

            raise AssertionError("unbalanced braces: %r" % (explanation,)) 

        end = start + i 

        where = end 

        if explanation[end - 1] == '\n': 

            explanation = (explanation[:start] + explanation[start+15:end-1] + 

                           explanation[end+1:]) 

            where -= 17 

    return explanation 

 

 

def _split_explanation(explanation): 

    """Return a list of individual lines in the explanation 

 

    This will return a list of lines split on '\n{', '\n}' and '\n~'. 

    Any other newlines will be escaped and appear in the line as the 

    literal '\n' characters. 

    """ 

    raw_lines = (explanation or u('')).split('\n') 

    lines = [raw_lines[0]] 

89    for l in raw_lines[1:]: 

        if l and l[0] in ['{', '}', '~', '>']: 

            lines.append(l) 

        else: 

            lines[-1] += '\\n' + l 

    return lines 

 

 

def _format_lines(lines): 

    """Format the individual lines 

 

    This will replace the '{', '}' and '~' characters of our mini 

    formatting language with the proper 'where ...', 'and ...' and ' + 

    ...' text, taking care of indentation along the way. 

 

    Return a list of formatted lines. 

    """ 

    result = lines[:1] 

    stack = [0] 

    stackcnt = [0] 

109    for line in lines[1:]: 

        if line.startswith('{'): 

            if stackcnt[-1]: 

                s = u('and   ') 

            else: 

                s = u('where ') 

            stack.append(len(result)) 

            stackcnt[-1] += 1 

            stackcnt.append(0) 

            result.append(u(' +') + u('  ')*(len(stack)-1) + s + line[1:]) 

        elif line.startswith('}'): 

            stack.pop() 

            stackcnt.pop() 

            result[stack[-1]] += line[1:] 

        else: 

            assert line[0] in ['~', '>'] 

            stack[-1] += 1 

            indent = len(stack) if line.startswith('~') else len(stack) - 1 

            result.append(u('  ')*indent + line[1:]) 

    assert len(stack) == 1 

    return result 

 

 

# Provide basestring in python3 

try: 

    basestring = basestring 

except NameError: 

    basestring = str 

 

 

def assertrepr_compare(config, op, left, right): 

    """Return specialised explanations for some operators/operands""" 

    width = 80 - 15 - len(op) - 2  # 15 chars indentation, 1 space around op 

    left_repr = py.io.saferepr(left, maxsize=int(width/2)) 

    right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) 

 

    summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr)) 

 

    issequence = lambda x: (isinstance(x, (list, tuple, Sequence)) and 

                            not isinstance(x, basestring)) 

    istext = lambda x: isinstance(x, basestring) 

    isdict = lambda x: isinstance(x, dict) 

    isset = lambda x: isinstance(x, (set, frozenset)) 

 

    def isiterable(obj): 

        try: 

            iter(obj) 

            return not istext(obj) 

        except TypeError: 

            return False 

 

    verbose = config.getoption('verbose') 

    explanation = None 

    try: 

        if op == '==': 

            if istext(left) and istext(right): 

                explanation = _diff_text(left, right, verbose) 

            else: 

                if issequence(left) and issequence(right): 

                    explanation = _compare_eq_sequence(left, right, verbose) 

                elif isset(left) and isset(right): 

                    explanation = _compare_eq_set(left, right, verbose) 

                elif isdict(left) and isdict(right): 

                    explanation = _compare_eq_dict(left, right, verbose) 

                if isiterable(left) and isiterable(right): 

                    expl = _compare_eq_iterable(left, right, verbose) 

                    if explanation is not None: 

                        explanation.extend(expl) 

                    else: 

                        explanation = expl 

        elif op == 'not in': 

            if istext(left) and istext(right): 

                explanation = _notin_text(left, right, verbose) 

    except Exception: 

        explanation = [ 

            u('(pytest_assertion plugin: representation of details failed.  ' 

              'Probably an object has a faulty __repr__.)'), 

            u(_pytest._code.ExceptionInfo())] 

 

    if not explanation: 

        return None 

 

    return [summary] + explanation 

 

 

def _diff_text(left, right, verbose=False): 

    """Return the explanation for the diff between text or bytes 

 

    Unless --verbose is used this will skip leading and trailing 

    characters which are identical to keep the diff minimal. 

 

    If the input are bytes they will be safely converted to text. 

    """ 

    from difflib import ndiff 

    explanation = [] 

    if isinstance(left, py.builtin.bytes): 

        left = u(repr(left)[1:-1]).replace(r'\n', '\n') 

    if isinstance(right, py.builtin.bytes): 

        right = u(repr(right)[1:-1]).replace(r'\n', '\n') 

    if not verbose: 

        i = 0  # just in case left or right has zero length 

        for i in range(min(len(left), len(right))): 

            if left[i] != right[i]: 

                break 

        if i > 42: 

            i -= 10                 # Provide some context 

            explanation = [u('Skipping %s identical leading ' 

                             'characters in diff, use -v to show') % i] 

            left = left[i:] 

            right = right[i:] 

        if len(left) == len(right): 

            for i in range(len(left)): 

                if left[-i] != right[-i]: 

                    break 

            if i > 42: 

                i -= 10     # Provide some context 

                explanation += [u('Skipping %s identical trailing ' 

                                  'characters in diff, use -v to show') % i] 

                left = left[:-i] 

                right = right[:-i] 

    explanation += [line.strip('\n') 

                    for line in ndiff(left.splitlines(), 

                                      right.splitlines())] 

    return explanation 

 

 

def _compare_eq_iterable(left, right, verbose=False): 

    if not verbose: 

        return [u('Use -v to get the full diff')] 

    # dynamic import to speedup pytest 

    import difflib 

 

    try: 

        left_formatting = pprint.pformat(left).splitlines() 

        right_formatting = pprint.pformat(right).splitlines() 

        explanation = [u('Full diff:')] 

    except Exception: 

        # hack: PrettyPrinter.pformat() in python 2 fails when formatting items that can't be sorted(), ie, calling 

        # sorted() on a list would raise. See issue #718. 

        # As a workaround, the full diff is generated by using the repr() string of each item of each container. 

        left_formatting = sorted(repr(x) for x in left) 

        right_formatting = sorted(repr(x) for x in right) 

        explanation = [u('Full diff (fallback to calling repr on each item):')] 

    explanation.extend(line.strip() for line in difflib.ndiff(left_formatting, right_formatting)) 

    return explanation 

 

 

def _compare_eq_sequence(left, right, verbose=False): 

    explanation = [] 

    for i in range(min(len(left), len(right))): 

        if left[i] != right[i]: 

            explanation += [u('At index %s diff: %r != %r') 

                            % (i, left[i], right[i])] 

            break 

    if len(left) > len(right): 

        explanation += [u('Left contains more items, first extra item: %s') 

                        % py.io.saferepr(left[len(right)],)] 

    elif len(left) < len(right): 

        explanation += [ 

            u('Right contains more items, first extra item: %s') % 

            py.io.saferepr(right[len(left)],)] 

    return explanation 

 

 

def _compare_eq_set(left, right, verbose=False): 

    explanation = [] 

    diff_left = left - right 

    diff_right = right - left 

    if diff_left: 

        explanation.append(u('Extra items in the left set:')) 

        for item in diff_left: 

            explanation.append(py.io.saferepr(item)) 

    if diff_right: 

        explanation.append(u('Extra items in the right set:')) 

        for item in diff_right: 

            explanation.append(py.io.saferepr(item)) 

    return explanation 

 

 

def _compare_eq_dict(left, right, verbose=False): 

    explanation = [] 

    common = set(left).intersection(set(right)) 

    same = dict((k, left[k]) for k in common if left[k] == right[k]) 

    if same and not verbose: 

        explanation += [u('Omitting %s identical items, use -v to show') % 

                        len(same)] 

    elif same: 

        explanation += [u('Common items:')] 

        explanation += pprint.pformat(same).splitlines() 

    diff = set(k for k in common if left[k] != right[k]) 

    if diff: 

        explanation += [u('Differing items:')] 

        for k in diff: 

            explanation += [py.io.saferepr({k: left[k]}) + ' != ' + 

                            py.io.saferepr({k: right[k]})] 

    extra_left = set(left) - set(right) 

    if extra_left: 

        explanation.append(u('Left contains more items:')) 

        explanation.extend(pprint.pformat( 

            dict((k, left[k]) for k in extra_left)).splitlines()) 

    extra_right = set(right) - set(left) 

    if extra_right: 

        explanation.append(u('Right contains more items:')) 

        explanation.extend(pprint.pformat( 

            dict((k, right[k]) for k in extra_right)).splitlines()) 

    return explanation 

 

 

def _notin_text(term, text, verbose=False): 

    index = text.find(term) 

    head = text[:index] 

    tail = text[index+len(term):] 

    correct_text = head + tail 

    diff = _diff_text(correct_text, text, verbose) 

    newdiff = [u('%s is contained here:') % py.io.saferepr(term, maxsize=42)] 

    for line in diff: 

        if line.startswith(u('Skipping')): 

            continue 

        if line.startswith(u('- ')): 

            continue 

        if line.startswith(u('+ ')): 

            newdiff.append(u('  ') + line[2:]) 

        else: 

            newdiff.append(line) 

    return newdiff