def _compare_eq_dict(left, right, verbose=0): explanation = [] common = set(left).intersection(set(right)) same = {k: left[k] for k in common if left[k] == right[k]} if same and verbose < 2: explanation += [u"Omitting %s identical items, use -vv to show" % len(same)] elif same: explanation += [u"Common items:"] explanation += pprint.pformat(same).splitlines() diff = {k for k in common if left[k] != right[k]} if diff: explanation += [u"Differing items:"] for k in diff: explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})] extra_left = set(left) - set(right) if extra_left: explanation.append(u"Left contains more items:") explanation.extend( pprint.pformat({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({k: right[k] for k in extra_right}).splitlines() ) return explanation
def get_real_func(obj): """ gets the real function object of the (possibly) wrapped object by functools.wraps or functools.partial. """ start_obj = obj for i in range(100): # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function # to trigger a warning if it gets called directly instead of by pytest: we don't # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774) new_obj = getattr(obj, "__pytest_wrapped__", None) if isinstance(new_obj, _PytestWrapper): obj = new_obj.obj break new_obj = getattr(obj, "__wrapped__", None) if new_obj is None: break obj = new_obj else: raise ValueError( ("could not find real function of {start}\nstopped at {current}").format( start=saferepr(start_obj), current=saferepr(obj) ) ) if isinstance(obj, functools.partial): obj = obj.func return obj
def _compare_eq_sequence(left, right, verbose=0): explanation = [] len_left = len(left) len_right = len(right) 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 len_diff = len_left - len_right if len_diff: if len_diff > 0: dir_with_more = "Left" extra = saferepr(left[len_right]) else: len_diff = 0 - len_diff dir_with_more = "Right" extra = saferepr(right[len_left]) if len_diff == 1: explanation += [u"%s contains one more item: %s" % (dir_with_more, extra)] else: explanation += [ u"%s contains %d more items, first extra item: %s" % (dir_with_more, len_diff, extra) ] return explanation
def _compare_eq_set(left, right, verbose=0): 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(saferepr(item)) if diff_right: explanation.append(u"Extra items in the right set:") for item in diff_right: explanation.append(saferepr(item)) return explanation
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 = saferepr(left, maxsize=int(width // 2)) right_repr = saferepr(right, maxsize=width - len(left_repr)) summary = u"%s %s %s" % (ecu(left_repr), op, ecu(right_repr)) 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) elif type(left) == type(right) and (isdatacls(left) or isattrs(left)): type_fn = (isdatacls, isattrs) explanation = _compare_eq_cls(left, right, verbose, type_fn) elif verbose > 0: explanation = _compare_eq_verbose(left, right) 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 outcomes.Exit: raise except Exception: explanation = [ u"(pytest_assertion plugin: representation of details failed. " u"Probably an object has a faulty __repr__.)", six.text_type(_pytest._code.ExceptionInfo.from_current()), ] if not explanation: return None return [summary] + explanation
def test_maxsize_error_on_instance(): class A: def __repr__(): raise ValueError("...") s = saferepr(("*" * 50, A()), maxsize=25) assert len(s) == 25 assert s[0] == "(" and s[-1] == ")"
def _compare_eq_sequence(left, right, verbose=0): 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" % saferepr(left[len(right)]) ] elif len(left) < len(right): explanation += [ u"Right contains more items, first extra item: %s" % saferepr(right[len(left)]) ] return explanation
def test_maxsize_error_on_instance(): class A: def __repr__(self): raise ValueError("...") s = saferepr(("*" * 50, A()), maxsize=25) assert len(s) == 25 assert s[0] == "(" and s[-1] == ")"
def test_buggy_builtin_repr(): # Simulate a case where a repr for a builtin raises. # reprlib dispatches by type name, so use "int". class int: def __repr__(self): raise ValueError("Buggy repr!") assert "Buggy" in saferepr(int())
def test_exceptions(): class BrokenRepr: def __init__(self, ex): self.ex = ex def __repr__(self): raise self.ex class BrokenReprException(Exception): __str__ = None __repr__ = None assert "Exception" in saferepr(BrokenRepr(Exception("broken"))) s = saferepr(BrokenReprException("really broken")) assert "TypeError" in s assert "TypeError" in saferepr(BrokenRepr("string")) s2 = saferepr(BrokenRepr(BrokenReprException("omg even worse"))) assert "NameError" not in s2 assert "unknown" in s2
def _saferepr(obj: object) -> str: r"""Get a safe repr of an object for assertion error messages. The assertion formatting (util.format_explanation()) requires newlines to be escaped since they are a special character for it. Normally assertion.util.format_explanation() does this but for a custom repr it is possible to contain one of the special escape sequences, especially '\n{' and '\n}' are likely to be present in JSON reprs. """ return saferepr(obj).replace("\n", "\\n")
def _compare_eq_dict(left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0) -> List[str]: explanation: List[str] = [] set_left = set(left) set_right = set(right) common = set_left.intersection(set_right) same = {k: left[k] for k in common if left[k] == right[k]} if same and verbose < 2: explanation += [ "Omitting %s identical items, use -vv to show" % len(same) ] elif same: explanation += ["Common items:"] explanation += pprint.pformat(same).splitlines() diff = {k for k in common if left[k] != right[k]} if diff: explanation += ["Differing items:"] for k in diff: explanation += [ saferepr({k: left[k]}) + " != " + saferepr({k: right[k]}) ] extra_left = set_left - set_right len_extra_left = len(extra_left) if len_extra_left: explanation.append( "Left contains %d more item%s:" % (len_extra_left, "" if len_extra_left == 1 else "s")) explanation.extend( pprint.pformat({k: left[k] for k in extra_left}).splitlines()) extra_right = set_right - set_left len_extra_right = len(extra_right) if len_extra_right: explanation.append( "Right contains %d more item%s:" % (len_extra_right, "" if len_extra_right == 1 else "s")) explanation.extend( pprint.pformat({k: right[k] for k in extra_right}).splitlines()) return explanation
def test_broken_getattribute(): """saferepr() can create proper representations of classes with broken __getattribute__ (#7145) """ class SomeClass: def __getattribute__(self, attr): raise RuntimeError def __repr__(self): raise RuntimeError assert saferepr(SomeClass()).startswith( "<[RuntimeError() raised in repr()] SomeClass object at 0x")
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]: """Return specialised explanations for some operators/operands""" verbose = config.getoption("verbose") if verbose > 1: left_repr = safeformat(left) right_repr = safeformat(right) else: # XXX: "15 chars indentation" is wrong # ("E AssertionError: assert "); should use term width. maxsize = ( 80 - 15 - len(op) - 2 ) // 2 # 15 chars indentation, 1 space around op left_repr = saferepr(left, maxsize=maxsize) right_repr = saferepr(right, maxsize=maxsize) summary = "{} {} {}".format(left_repr, op, right_repr) explanation = None try: if op == "==": explanation = _compare_eq_any(left, right, verbose) elif op == "not in": if istext(left) and istext(right): explanation = _notin_text(left, right, verbose) except outcomes.Exit: raise except Exception: explanation = [ "(pytest_assertion plugin: representation of details failed: {}.".format( _pytest._code.ExceptionInfo.from_current()._getreprcrash() ), " Probably an object has a faulty __repr__.)", ] if not explanation: return None return [summary] + explanation
def get_real_func(obj): """Get the real function object of the (possibly) wrapped object by functools.wraps or functools.partial.""" start_obj = obj for i in range(100): # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function # to trigger a warning if it gets called directly instead of by pytest: we don't # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774) new_obj = getattr(obj, "__pytest_wrapped__", None) if isinstance(new_obj, _PytestWrapper): obj = new_obj.obj break new_obj = getattr(obj, "__wrapped__", None) if new_obj is None: break obj = new_obj else: raise ValueError( ("could not find real function of {start}\nstopped at {current}" ).format(start=saferepr(start_obj), current=saferepr(obj))) if isinstance(obj, functools.partial): obj = obj.func return obj
def _notin_text(term, text, verbose=0): index = text.find(term) head = text[:index] tail = text[index + len(term):] correct_text = head + tail diff = _diff_text(correct_text, text, verbose) newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)] for line in diff: if line.startswith("Skipping"): continue if line.startswith("- "): continue if line.startswith("+ "): newdiff.append(" " + line[2:]) else: newdiff.append(line) return newdiff
def _notin_text(term, text, verbose=0): 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:" % 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
def _saferepr(obj): """Get a safe repr of an object for assertion error messages. The assertion formatting (util.format_explanation()) requires newlines to be escaped since they are a special character for it. Normally assertion.util.format_explanation() does this but for a custom repr it is possible to contain one of the special escape sequences, especially '\n{' and '\n}' are likely to be present in JSON reprs. """ r = saferepr(obj) # only occurs in python2.x, repr must return text in python3+ if isinstance(r, bytes): # Represent unprintable bytes as `\x##` r = u"".join(u"\\x{:x}".format(ord(c)) if c not in string.printable else c.decode() for c in r) return r.replace(u"\n", u"\\n")
def _saferepr(obj): """Get a safe repr of an object for assertion error messages. The assertion formatting (util.format_explanation()) requires newlines to be escaped since they are a special character for it. Normally assertion.util.format_explanation() does this but for a custom repr it is possible to contain one of the special escape sequences, especially '\n{' and '\n}' are likely to be present in JSON reprs. """ r = saferepr(obj) # only occurs in python2.x, repr must return text in python3+ if isinstance(r, bytes): # Represent unprintable bytes as `\x##` r = u"".join( u"\\x{:x}".format(ord(c)) if c not in string.printable else c.decode() for c in r ) return r.replace(u"\n", u"\\n")
def _format_assertmsg(obj: object) -> str: r"""Format the custom assertion message given. For strings this simply replaces newlines with '\n~' so that util.format_explanation() will preserve them instead of escaping newlines. For other objects saferepr() is used first. """ # reprlib appears to have a bug which means that if a string # contains a newline it gets escaped, however if an object has a # .__repr__() which contains newlines it does not get escaped. # However in either case we want to preserve the newline. replaces = [("\n", "\n~"), ("%", "%%")] if not isinstance(obj, str): obj = saferepr(obj) replaces.append(("\\n", "\n~")) for r1, r2 in replaces: obj = obj.replace(r1, r2) return obj
def from_current(cls, exprinfo=None): """returns an ExceptionInfo matching the current traceback .. warning:: Experimental API :param exprinfo: a text string helping to determine if we should strip ``AssertionError`` from the output, defaults to the exception message/``__str__()`` """ tup = sys.exc_info() _striptext = "" if exprinfo is None and isinstance(tup[1], AssertionError): exprinfo = getattr(tup[1], "msg", None) if exprinfo is None: exprinfo = saferepr(tup[1]) if exprinfo and exprinfo.startswith(cls._assert_start_repr): _striptext = "AssertionError: " return cls(tup, _striptext)
def from_current(cls, exprinfo=None): """returns an ExceptionInfo matching the current traceback .. warning:: Experimental API :param exprinfo: a text string helping to determine if we should strip ``AssertionError`` from the output, defaults to the exception message/``__str__()`` """ tup = sys.exc_info() assert tup[0] is not None, "no current exception" _striptext = "" if exprinfo is None and isinstance(tup[1], AssertionError): exprinfo = getattr(tup[1], "msg", None) if exprinfo is None: exprinfo = saferepr(tup[1]) if exprinfo and exprinfo.startswith(cls._assert_start_repr): _striptext = "AssertionError: " return cls(tup, _striptext)
def from_exc_info( cls, exc_info: Tuple["Type[_E]", "_E", TracebackType], exprinfo: Optional[str] = None, ) -> "ExceptionInfo[_E]": """Returns an ExceptionInfo for an existing exc_info tuple. .. warning:: Experimental API :param exprinfo: a text string helping to determine if we should strip ``AssertionError`` from the output, defaults to the exception message/``__str__()`` """ _striptext = "" if exprinfo is None and isinstance(exc_info[1], AssertionError): exprinfo = getattr(exc_info[1], "msg", None) if exprinfo is None: exprinfo = saferepr(exc_info[1]) if exprinfo and exprinfo.startswith(cls._assert_start_repr): _striptext = "AssertionError: " return cls(exc_info, _striptext)
def _format_assertmsg(obj): """Format the custom assertion message given. For strings this simply replaces newlines with '\n~' so that util.format_explanation() will preserve them instead of escaping newlines. For other objects saferepr() is used first. """ # reprlib appears to have a bug which means that if a string # contains a newline it gets escaped, however if an object has a # .__repr__() which contains newlines it does not get escaped. # However in either case we want to preserve the newline. replaces = [(u"\n", u"\n~"), (u"%", u"%%")] if not isinstance(obj, six.string_types): obj = saferepr(obj) replaces.append((u"\\n", u"\n~")) if isinstance(obj, bytes): replaces = [(r1.encode(), r2.encode()) for r1, r2 in replaces] for r1, r2 in replaces: obj = obj.replace(r1, r2) return obj
def test_no_maxsize(): text = "x" * DEFAULT_REPR_MAX_SIZE * 10 s = saferepr(text, maxsize=None) expected = repr(text) assert s == expected
def _saferepr(self, obj): return saferepr(obj)
def test_unicode(): val = "£€" reprval = "'£€'" assert saferepr(val) == reprval
def test_repr_on_newstyle() -> None: class Function: def __repr__(self): return "<%s>" % (self.name) # type: ignore[attr-defined] assert saferepr(Function())
def test_big_repr(): from _pytest._io.saferepr import SafeRepr assert len(saferepr( range(1000))) <= len("[" + SafeRepr(0).maxlist * "1000" + "]")
def test_big_repr(): from _pytest._io.saferepr import SafeRepr assert len(saferepr(range(1000))) <= len("[" + SafeRepr().maxlist * "1000" + "]")
def repr(self, object: object) -> str: """Return a 'safe' (non-recursive, one-line) string repr for 'object'.""" return saferepr(object)
def repr(self, object): """ return a 'safe' (non-recursive, one-line) string repr for 'object' """ return saferepr(object)
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 = saferepr(left, maxsize=int(width // 2)) right_repr = saferepr(right, maxsize=width - len(left_repr)) summary = u"%s %s %s" % (ecu(left_repr), op, ecu(right_repr)) def issequence(x): return isinstance(x, Sequence) and not isinstance(x, basestring) def istext(x): return isinstance(x, basestring) def isdict(x): return isinstance(x, dict) def isset(x): return isinstance(x, (set, frozenset)) def isdatacls(obj): return getattr(obj, "__dataclass_fields__", None) is not None def isattrs(obj): return getattr(obj, "__attrs_attrs__", None) is not None 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) elif type(left) == type(right) and (isdatacls(left) or isattrs(left)): type_fn = (isdatacls, isattrs) explanation = _compare_eq_cls(left, right, verbose, type_fn) elif verbose: explanation = _compare_eq_verbose(left, right) 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. " u"Probably an object has a faulty __repr__.)", six.text_type(_pytest._code.ExceptionInfo.from_current()), ] if not explanation: return None return [summary] + explanation
def getdecoded(out): try: return out.decode("utf-8") except UnicodeDecodeError: return "INTERNAL not-utf8-decodeable, truncated string:\n{}".format( saferepr(out))
def test_maxsize(): s = saferepr("x" * 50, maxsize=25) assert len(s) == 25 expected = repr("x" * 10 + "..." + "x" * 10) assert s == expected
def test_unicode(): val = u"£€" reprval = u"'£€'" assert saferepr(val) == reprval
def test_repr_on_newstyle(): class Function(object): def __repr__(self): return "<%s>" % (self.name) assert saferepr(Function())
def test_simple_repr(): assert saferepr(1) == "1" assert saferepr(None) == "None"
def test_baseexception(): """Test saferepr() with BaseExceptions, which includes pytest outcomes.""" class RaisingOnStrRepr(BaseException): def __init__(self, exc_types): self.exc_types = exc_types def raise_exc(self, *args): try: self.exc_type = self.exc_types.pop(0) except IndexError: pass if hasattr(self.exc_type, "__call__"): raise self.exc_type(*args) raise self.exc_type def __str__(self): self.raise_exc("__str__") def __repr__(self): self.raise_exc("__repr__") class BrokenObj: def __init__(self, exc): self.exc = exc def __repr__(self): raise self.exc __str__ = __repr__ baseexc_str = BaseException("__str__") obj = BrokenObj(RaisingOnStrRepr([BaseException])) assert saferepr(obj) == ( "<[unpresentable exception ({!r}) " "raised in repr()] BrokenObj object at 0x{:x}>".format( baseexc_str, id(obj))) obj = BrokenObj(RaisingOnStrRepr([RaisingOnStrRepr([BaseException])])) assert saferepr(obj) == ( "<[{!r} raised in repr()] BrokenObj object at 0x{:x}>".format( baseexc_str, id(obj))) with pytest.raises(KeyboardInterrupt): saferepr(BrokenObj(KeyboardInterrupt())) with pytest.raises(SystemExit): saferepr(BrokenObj(SystemExit())) with pytest.raises(KeyboardInterrupt): saferepr(BrokenObj(RaisingOnStrRepr([KeyboardInterrupt]))) with pytest.raises(SystemExit): saferepr(BrokenObj(RaisingOnStrRepr([SystemExit]))) with pytest.raises(KeyboardInterrupt): print( saferepr( BrokenObj(RaisingOnStrRepr([BaseException, KeyboardInterrupt])))) with pytest.raises(SystemExit): saferepr(BrokenObj(RaisingOnStrRepr([BaseException, SystemExit])))
def __repr__(self) -> str: if self._excinfo is None: return "<ExceptionInfo for raises contextmanager>" return "<{} {} tblen={}>".format(self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback))
def getdecoded(out): try: return out.decode("utf-8") except UnicodeDecodeError: return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (saferepr(out),)
def repr_args(self, entry): if self.funcargs: args = [] for argname, argvalue in entry.frame.getargs(var=True): args.append((argname, saferepr(argvalue))) return ReprFuncArgs(args)