From 13932b7f4bebc210dc75164f2e7fbffa7acb5f8a Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 27 Aug 2009 17:26:02 +0200 Subject: [PATCH] * deprecate py.magic.invoke/revoke in favour of the new py.code.patch_builtins, py.code.unpatch_builtins * deprecate py.magic.patch/revert * deprecate py.magic.AssertionError in favour of py.code._AssertionError * introduced pytest_assertion plugin. --HG-- branch : trunk --- py/__init__.py | 13 +- py/_testing/test_api.py | 49 ------ py/_testing/test_initpkg.py | 11 ++ py/{magic/exprinfo.py => code/assertion.py} | 140 ++++++++++++++-- py/code/code.py | 33 +++- py/code/oldmagic.py | 61 +++++++ py/code/oldmagic2.py | 6 + py/code/testing/test_assertion.py | 173 ++++++++++++++++++++ py/code/testing/test_code.py | 17 +- py/code/testing/test_excinfo.py | 4 +- py/code/testing/test_oldmagic.py | 64 ++++++++ py/magic/assertion.py | 38 ----- py/magic/invoke.py | 24 --- py/magic/patch.py | 26 --- py/magic/testing/test_assertion.py | 106 ------------ py/magic/testing/test_exprinfo.py | 156 ------------------ py/magic/testing/test_invoke.py | 29 ---- py/magic/testing/test_patch.py | 31 ---- py/magic/testing/test_viewtype.py | 55 ------- py/magic/viewtype.py | 94 ----------- py/test/cmdline.py | 9 - py/test/defaultconftest.py | 2 +- py/test/plugin/pytest_assertion.py | 68 ++++++++ py/test/plugin/test_pytest_terminal.py | 30 ---- py/test/pycollect.py | 8 - py/test/testing/test_pycollect.py | 13 -- 26 files changed, 564 insertions(+), 696 deletions(-) rename py/{magic/exprinfo.py => code/assertion.py} (76%) create mode 100644 py/code/oldmagic.py create mode 100644 py/code/oldmagic2.py create mode 100644 py/code/testing/test_assertion.py create mode 100644 py/code/testing/test_oldmagic.py delete mode 100644 py/magic/assertion.py delete mode 100644 py/magic/invoke.py delete mode 100644 py/magic/patch.py delete mode 100644 py/magic/testing/test_assertion.py delete mode 100644 py/magic/testing/test_exprinfo.py delete mode 100644 py/magic/testing/test_invoke.py delete mode 100644 py/magic/testing/test_patch.py delete mode 100644 py/magic/testing/test_viewtype.py delete mode 100644 py/magic/viewtype.py create mode 100644 py/test/plugin/pytest_assertion.py diff --git a/py/__init__.py b/py/__init__.py index cbedf508d..ffecca12c 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -114,12 +114,12 @@ initpkg(__name__, # some nice slightly magic APIs 'magic.__doc__' : ('./magic/__init__.py', '__doc__'), - 'magic.invoke' : ('./magic/invoke.py', 'invoke'), - 'magic.revoke' : ('./magic/invoke.py', 'revoke'), - 'magic.patch' : ('./magic/patch.py', 'patch'), - 'magic.revert' : ('./magic/patch.py', 'revert'), + 'magic.invoke' : ('./code/oldmagic.py', 'invoke'), + 'magic.revoke' : ('./code/oldmagic.py', 'revoke'), + 'magic.patch' : ('./code/oldmagic.py', 'patch'), + 'magic.revert' : ('./code/oldmagic.py', 'revert'), 'magic.autopath' : ('./magic/autopath.py', 'autopath'), - 'magic.AssertionError' : ('./magic/assertion.py', 'AssertionError'), + 'magic.AssertionError' : ('./code/oldmagic2.py', 'AssertionError'), # python inspection/code-generation API 'code.__doc__' : ('./code/__init__.py', '__doc__'), @@ -130,6 +130,9 @@ initpkg(__name__, 'code.ExceptionInfo' : ('./code/code.py', 'ExceptionInfo'), 'code.Traceback' : ('./code/code.py', 'Traceback'), 'code.getfslineno' : ('./code/source.py', 'getfslineno'), + 'code.patch_builtins' : ('./code/code.py', 'patch_builtins'), + 'code.unpatch_builtins' : ('./code/code.py', 'unpatch_builtins'), + 'code._AssertionError' : ('./code/assertion.py', 'AssertionError'), # backports and additions of builtins 'builtin.__doc__' : ('./builtin/__init__.py', '__doc__'), diff --git a/py/_testing/test_api.py b/py/_testing/test_api.py index 07d6defbf..033efe5c8 100644 --- a/py/_testing/test_api.py +++ b/py/_testing/test_api.py @@ -4,52 +4,3 @@ import py import sys import inspect -def test_all_resolves(): - seen = py.builtin.set([py]) - lastlength = None - while len(seen) != lastlength: - lastlength = len(seen) - for item in py.builtin.frozenset(seen): - for value in item.__dict__.values(): - if isinstance(value, type(py.test)): - seen.add(value) - - -class TestAPI_V0_namespace_consistence: - def test_path_entrypoints(self): - assert inspect.ismodule(py.path) - assert_class('py.path', 'local') - assert_class('py.path', 'svnwc') - assert_class('py.path', 'svnurl') - - def test_magic_entrypoints(self): - assert_function('py.magic', 'invoke') - assert_function('py.magic', 'revoke') - assert_function('py.magic', 'patch') - assert_function('py.magic', 'revoke') - - def test_process_entrypoints(self): - assert_function('py.process', 'cmdexec') - - def XXXtest_utest_entrypoints(self): - # XXX TOBECOMPLETED - assert_function('py.test', 'main') - #assert_module('std.utest', 'collect') - -def assert_class(modpath, name): - mod = __import__(modpath, None, None, [name]) - obj = getattr(mod, name) - assert inspect.isclass(obj) - - # we don't test anymore that the exported classes have - # the exported module path and name on them. - #fullpath = modpath + '.' + name - #assert obj.__module__ == modpath - #if sys.version_info >= (2,3): - # assert obj.__name__ == name - -def assert_function(modpath, name): - mod = __import__(modpath, None, None, [name]) - obj = getattr(mod, name) - assert hasattr(obj, 'func_doc') - #assert obj.func_name == name diff --git a/py/_testing/test_initpkg.py b/py/_testing/test_initpkg.py index 5da9e6c29..17cd5c2d8 100644 --- a/py/_testing/test_initpkg.py +++ b/py/_testing/test_initpkg.py @@ -174,3 +174,14 @@ def test_autoimport(): from py.initpkg import autoimport py.std.os.environ['AUTOTEST_AUTOIMPORT'] = "nonexistmodule" py.test.raises(ImportError, "autoimport('autotest')") + + +def test_all_resolves(): + seen = py.builtin.set([py]) + lastlength = None + while len(seen) != lastlength: + lastlength = len(seen) + for item in py.builtin.frozenset(seen): + for value in item.__dict__.values(): + if isinstance(value, type(py.test)): + seen.add(value) diff --git a/py/magic/exprinfo.py b/py/code/assertion.py similarity index 76% rename from py/magic/exprinfo.py rename to py/code/assertion.py index 369e759d5..4ed6118bc 100644 --- a/py/magic/exprinfo.py +++ b/py/code/assertion.py @@ -1,6 +1,8 @@ -from compiler import parse, ast, pycodegen import py -import __builtin__, sys +import sys, inspect +from compiler import parse, ast, pycodegen +import __builtin__ as cpy_builtin +BuiltinAssertionError = cpy_builtin.AssertionError passthroughex = (KeyboardInterrupt, SystemExit, MemoryError) @@ -8,10 +10,92 @@ class Failure: def __init__(self, node): self.exc, self.value, self.tb = sys.exc_info() self.node = node - #import traceback - #traceback.print_exc() -from py.__.magic.viewtype import View +class View(object): + """View base class. + + If C is a subclass of View, then C(x) creates a proxy object around + the object x. The actual class of the proxy is not C in general, + but a *subclass* of C determined by the rules below. To avoid confusion + we call view class the class of the proxy (a subclass of C, so of View) + and object class the class of x. + + Attributes and methods not found in the proxy are automatically read on x. + Other operations like setting attributes are performed on the proxy, as + determined by its view class. The object x is available from the proxy + as its __obj__ attribute. + + The view class selection is determined by the __view__ tuples and the + optional __viewkey__ method. By default, the selected view class is the + most specific subclass of C whose __view__ mentions the class of x. + If no such subclass is found, the search proceeds with the parent + object classes. For example, C(True) will first look for a subclass + of C with __view__ = (..., bool, ...) and only if it doesn't find any + look for one with __view__ = (..., int, ...), and then ..., object,... + If everything fails the class C itself is considered to be the default. + + Alternatively, the view class selection can be driven by another aspect + of the object x, instead of the class of x, by overriding __viewkey__. + See last example at the end of this module. + """ + + _viewcache = {} + __view__ = () + + def __new__(rootclass, obj, *args, **kwds): + self = object.__new__(rootclass) + self.__obj__ = obj + self.__rootclass__ = rootclass + key = self.__viewkey__() + try: + self.__class__ = self._viewcache[key] + except KeyError: + self.__class__ = self._selectsubclass(key) + return self + + def __getattr__(self, attr): + # attributes not found in the normal hierarchy rooted on View + # are looked up in the object's real class + return getattr(self.__obj__, attr) + + def __viewkey__(self): + return self.__obj__.__class__ + + def __matchkey__(self, key, subclasses): + if inspect.isclass(key): + keys = inspect.getmro(key) + else: + keys = [key] + for key in keys: + result = [C for C in subclasses if key in C.__view__] + if result: + return result + return [] + + def _selectsubclass(self, key): + subclasses = list(enumsubclasses(self.__rootclass__)) + for C in subclasses: + if not isinstance(C.__view__, tuple): + C.__view__ = (C.__view__,) + choices = self.__matchkey__(key, subclasses) + if not choices: + return self.__rootclass__ + elif len(choices) == 1: + return choices[0] + else: + # combine the multiple choices + return type('?', tuple(choices), {}) + + def __repr__(self): + return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__) + + +def enumsubclasses(cls): + for subcls in cls.__subclasses__(): + for subsubclass in enumsubclasses(subcls): + yield subsubclass + yield cls + class Interpretable(View): """A parse tree node with a few extra methods.""" @@ -322,8 +406,6 @@ class Getattr(Interpretable): self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation) # == Re-interpretation of full statements == -import __builtin__ -BuiltinAssertionError = __builtin__.AssertionError class Assert(Interpretable): __view__ = ast.Assert @@ -390,7 +472,7 @@ def report_failure(e): explanation = ", in: " + explanation else: explanation = "" - print "%s: %s%s" % (e.exc.__name__, e.value, explanation) + sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation)) def check(s, frame=None): if frame is None: @@ -404,11 +486,12 @@ def check(s, frame=None): node.eval(frame) except passthroughex: raise - except Failure, e: + except Failure: + e = sys.exc_info()[1] report_failure(e) else: if not frame.is_true(node.result): - print "assertion failed:", node.nice_explanation() + sys.stderr.write("assertion failed: %s\n" % node.nice_explanation()) ########################################################### @@ -422,7 +505,8 @@ def interpret(source, frame, should_fail=False): frame = py.code.Frame(frame) try: module.run(frame) - except Failure, e: + except Failure: + e = sys.exc_info()[1] return getfailure(e) except passthroughex: raise @@ -447,7 +531,7 @@ def getmsg(excinfo): source = str(tb.statement).strip() x = interpret(source, tb.frame, should_fail=True) if not isinstance(x, str): - raise TypeError, "interpret returned non-string %r" % (x,) + raise TypeError("interpret returned non-string %r" % (x,)) return x def getfailure(e): @@ -469,10 +553,40 @@ def run(s, frame=None): module = Interpretable(parse(s, 'exec').node) try: module.run(frame) - except Failure, e: + except Failure: + e = sys.exc_info()[1] report_failure(e) +class AssertionError(BuiltinAssertionError): + def __init__(self, *args): + BuiltinAssertionError.__init__(self, *args) + if args: + try: + self.msg = str(args[0]) + except (KeyboardInterrupt, SystemExit): + raise + except: + self.msg = "<[broken __repr__] %s at %0xd>" %( + args[0].__class__, id(args[0])) + + else: + f = sys._getframe(1) + try: + source = py.code.Frame(f).statement + source = str(source.deindent()).strip() + except py.error.ENOENT: + source = None + # this can also occur during reinterpretation, when the + # co_filename is set to "". + if source: + self.msg = interpret(source, f, should_fail=True) + if not self.args: + self.args = (self.msg,) + else: + self.msg = None + + if __name__ == '__main__': # example: def f(): diff --git a/py/code/code.py b/py/code/code.py index c02ac81e9..34e6445b1 100644 --- a/py/code/code.py +++ b/py/code/code.py @@ -5,7 +5,9 @@ try: except ImportError: import reprlib as repr -from __builtin__ import repr as builtin_repr +import __builtin__ as cpy_builtin + +builtin_repr = cpy_builtin.repr class Code(object): """ wrapper around Python code objects """ @@ -194,9 +196,9 @@ class TracebackEntry(object): """Reinterpret the failing statement and returns a detailed information about what operations are performed.""" if self.exprinfo is None: - from py.__.magic import exprinfo + from py.__.code import assertion source = str(self.statement).strip() - x = exprinfo.interpret(source, self.frame, should_fail=True) + x = assertion.interpret(source, self.frame, should_fail=True) if not isinstance(x, str): raise TypeError, "interpret returned non-string %r" % (x,) self.exprinfo = x @@ -355,7 +357,7 @@ class ExceptionInfo(object): # ExceptionInfo-like classes may have different attributes. if tup is None: tup = sys.exc_info() - if exprinfo is None and isinstance(tup[1], py.magic.AssertionError): + if exprinfo is None and isinstance(tup[1], py.code._AssertionError): exprinfo = tup[1].msg if exprinfo and exprinfo.startswith('assert '): self._striptext = 'AssertionError: ' @@ -371,7 +373,7 @@ class ExceptionInfo(object): """ return the exception as a string when 'tryshort' resolves to True, and the exception is a - py.magic.AssertionError, only the actual exception part of + py.code._AssertionError, only the actual exception part of the exception representation is returned (so 'AssertionError: ' is removed from the beginning) """ @@ -727,3 +729,24 @@ class SafeRepr(repr.Repr): return s safe_repr = SafeRepr().repr + +oldbuiltins = {} + +def patch_builtins(assertion=True, compile=True): + """ put compile and AssertionError builtins to Python's builtins. """ + if assertion: + from py.__.code import assertion + l = oldbuiltins.setdefault('AssertionError', []) + l.append(cpy_builtin.AssertionError) + cpy_builtin.AssertionError = assertion.AssertionError + if compile: + l = oldbuiltins.setdefault('compile', []) + l.append(cpy_builtin.compile) + cpy_builtin.compile = py.code.compile + +def unpatch_builtins(assertion=True, compile=True): + """ remove compile and AssertionError builtins from Python builtins. """ + if assertion: + cpy_builtin.AssertionError = oldbuiltins['AssertionError'].pop() + if compile: + cpy_builtin.compile = oldbuiltins['compile'].pop() diff --git a/py/code/oldmagic.py b/py/code/oldmagic.py new file mode 100644 index 000000000..6cfbc5d2b --- /dev/null +++ b/py/code/oldmagic.py @@ -0,0 +1,61 @@ +""" deprecated module for turning on/off some features. """ + +import py +import __builtin__ as cpy_builtin + +def invoke(assertion=False, compile=False): + """ (deprecated) invoke magic, currently you can specify: + + assertion patches the builtin AssertionError to try to give + more meaningful AssertionErrors, which by means + of deploying a mini-interpreter constructs + a useful error message. + """ + py.log._apiwarn("1.1", + "py.magic.invoke() is deprecated, use py.code.patch_builtins()", + stacklevel=2, + ) + py.code.patch_builtins(assertion=assertion, compile=compile) + +def revoke(assertion=False, compile=False): + """ (deprecated) revoke previously invoked magic (see invoke()).""" + py.log._apiwarn("1.1", + "py.magic.revoke() is deprecated, use py.code.unpatch_builtins()", + stacklevel=2, + ) + py.code.unpatch_builtins(assertion=assertion, compile=compile) + +patched = {} + +def patch(namespace, name, value): + """ (deprecated) rebind the 'name' on the 'namespace' to the 'value', + possibly and remember the original value. Multiple + invocations to the same namespace/name pair will + remember a list of old values. + """ + py.log._apiwarn("1.1", + "py.magic.patch() is deprecated, in tests use monkeypatch funcarg.", + stacklevel=2, + ) + nref = (namespace, name) + orig = getattr(namespace, name) + patched.setdefault(nref, []).append(orig) + setattr(namespace, name, value) + return orig + +def revert(namespace, name): + """ (deprecated) revert to the orginal value the last patch modified. + Raise ValueError if no such original value exists. + """ + py.log._apiwarn("1.1", + "py.magic.revert() is deprecated, in tests use monkeypatch funcarg.", + stacklevel=2, + ) + nref = (namespace, name) + if nref not in patched or not patched[nref]: + raise ValueError, "No original value stored for %s.%s" % nref + current = getattr(namespace, name) + orig = patched[nref].pop() + setattr(namespace, name, orig) + return current + diff --git a/py/code/oldmagic2.py b/py/code/oldmagic2.py new file mode 100644 index 000000000..d041a800f --- /dev/null +++ b/py/code/oldmagic2.py @@ -0,0 +1,6 @@ + +import py + +py.log._apiwarn("1.1", "py.magic.AssertionError is deprecated, use py.code._AssertionError", stacklevel=2) + +from py.code import _AssertionError as AssertionError diff --git a/py/code/testing/test_assertion.py b/py/code/testing/test_assertion.py new file mode 100644 index 000000000..dc8dfeac3 --- /dev/null +++ b/py/code/testing/test_assertion.py @@ -0,0 +1,173 @@ +import py +from py.__.code.assertion import View + +def setup_module(mod): + py.code.patch_builtins(assertion=True, compile=False) + +def teardown_module(mod): + py.code.unpatch_builtins(assertion=True, compile=False) + +def f(): + return 2 + +def test_assert(): + try: + assert f() == 3 + except AssertionError, e: + s = str(e) + assert s.startswith('assert 2 == 3\n') + +def test_assert_with_explicit_message(): + try: + assert f() == 3, "hello" + except AssertionError, e: + assert e.msg == 'hello' + +def test_assert_within_finally(): + class A: + def f(): + pass + excinfo = py.test.raises(TypeError, """ + try: + A().f() + finally: + i = 42 + """) + s = excinfo.exconly() + assert s.find("takes no argument") != -1 + + #def g(): + # A.f() + #excinfo = getexcinfo(TypeError, g) + #msg = getmsg(excinfo) + #assert msg.find("must be called with A") != -1 + + +def test_assert_multiline_1(): + try: + assert (f() == + 3) + except AssertionError, e: + s = str(e) + assert s.startswith('assert 2 == 3\n') + +def test_assert_multiline_2(): + try: + assert (f() == (4, + 3)[-1]) + except AssertionError, e: + s = str(e) + assert s.startswith('assert 2 ==') + +def test_assert_non_string_message(): + class A: + def __str__(self): + return "hello" + try: + assert 0 == 1, A() + except AssertionError, e: + assert e.msg == "hello" + + +# These tests should both fail, but should fail nicely... +class WeirdRepr: + def __repr__(self): + return '' + +def bug_test_assert_repr(): + v = WeirdRepr() + try: + assert v == 1 + except AssertionError, e: + assert e.msg.find('WeirdRepr') != -1 + assert e.msg.find('second line') != -1 + assert 0 + +def test_assert_non_string(): + try: + assert 0, ['list'] + except AssertionError, e: + assert e.msg.find("list") != -1 + +def test_assert_implicit_multiline(): + try: + x = [1,2,3] + assert x != [1, + 2, 3] + except AssertionError, e: + assert e.msg.find('assert [1, 2, 3] !=') != -1 + + +def test_assert_with_brokenrepr_arg(): + class BrokenRepr: + def __repr__(self): 0 / 0 + e = AssertionError(BrokenRepr()) + if e.msg.find("broken __repr__") == -1: + py.test.fail("broken __repr__ not handle correctly") + + +class TestView: + def test_class_dispatch(self): + ### Use a custom class hierarchy with existing instances + + class Picklable(View): + pass + + class Simple(Picklable): + __view__ = object + def pickle(self): + return repr(self.__obj__) + + class Seq(Picklable): + __view__ = list, tuple, dict + def pickle(self): + return ';'.join( + [Picklable(item).pickle() for item in self.__obj__]) + + class Dict(Seq): + __view__ = dict + def pickle(self): + return Seq.pickle(self) + '!' + Seq(self.values()).pickle() + + assert Picklable(123).pickle() == '123' + assert Picklable([1,[2,3],4]).pickle() == '1;2;3;4' + assert Picklable({1:2}).pickle() == '1!2' + + def test_viewtype_class_hierarchy(self): + # Use a custom class hierarchy based on attributes of existing instances + class Operation: + "Existing class that I don't want to change." + def __init__(self, opname, *args): + self.opname = opname + self.args = args + + existing = [Operation('+', 4, 5), + Operation('getitem', '', 'join'), + Operation('setattr', 'x', 'y', 3), + Operation('-', 12, 1)] + + class PyOp(View): + def __viewkey__(self): + return self.opname + def generate(self): + return '%s(%s)' % (self.opname, ', '.join(map(repr, self.args))) + + class PyBinaryOp(PyOp): + __view__ = ('+', '-', '*', '/') + def generate(self): + return '%s %s %s' % (self.args[0], self.opname, self.args[1]) + + codelines = [PyOp(op).generate() for op in existing] + assert codelines == ["4 + 5", "getitem('', 'join')", + "setattr('x', 'y', 3)", "12 - 1"] + +def test_AssertionError(testdir): + testdir.makepyfile(""" + import py + def test_hello(recwarn): + err = py.magic.AssertionError + recwarn.pop(DeprecationWarning) + assert err is py.code._AssertionError + """) + result = testdir.runpytest() + assert "1 passed" in result.stdout.str() diff --git a/py/code/testing/test_code.py b/py/code/testing/test_code.py index 53e32ddfd..82f478dfb 100644 --- a/py/code/testing/test_code.py +++ b/py/code/testing/test_code.py @@ -164,5 +164,18 @@ class TestSafeRepr: s = safe_repr(Function()) except Exception, e: py.test.fail("saferepr failed for newstyle class") - - + +def test_builtin_patch_unpatch(monkeypatch): + import __builtin__ as cpy_builtin + comp = cpy_builtin.compile + def mycompile(*args, **kwargs): + return comp(*args, **kwargs) + monkeypatch.setattr(cpy_builtin, 'AssertionError', None) + monkeypatch.setattr(cpy_builtin, 'compile', mycompile) + py.code.patch_builtins() + assert cpy_builtin.AssertionError + assert cpy_builtin.compile != mycompile + py.code.unpatch_builtins() + assert cpy_builtin.AssertionError is None + assert cpy_builtin.compile == mycompile + diff --git a/py/code/testing/test_excinfo.py b/py/code/testing/test_excinfo.py index 237607ed3..720cab1e7 100644 --- a/py/code/testing/test_excinfo.py +++ b/py/code/testing/test_excinfo.py @@ -577,11 +577,11 @@ raise ValueError() x = 1 assert x == 2 """) - py.magic.invoke(assertion=True) + py.code.patch_builtins(assertion=True) try: excinfo = py.test.raises(AssertionError, mod.somefunc) finally: - py.magic.revoke(assertion=True) + py.code.unpatch_builtins(assertion=True) p = FormattedExcinfo() reprentry = p.repr_traceback_entry(excinfo.traceback[-1], excinfo) diff --git a/py/code/testing/test_oldmagic.py b/py/code/testing/test_oldmagic.py new file mode 100644 index 000000000..2d20031c0 --- /dev/null +++ b/py/code/testing/test_oldmagic.py @@ -0,0 +1,64 @@ +import py + +def check_assertion(): + excinfo = py.test.raises(AssertionError, "assert 1 == 2") + s = excinfo.exconly(tryshort=True) + if not s == "assert 1 == 2": + raise ValueError("assertion not enabled: got %s" % s) + +def test_invoke_assertion(recwarn, monkeypatch): + monkeypatch.setattr(py.std.__builtin__, 'AssertionError', None) + py.magic.invoke(assertion=True) + try: + check_assertion() + finally: + py.magic.revoke(assertion=True) + recwarn.pop(DeprecationWarning) + +def test_invoke_compile(recwarn, monkeypatch): + monkeypatch.setattr(py.std.__builtin__, 'compile', None) + py.magic.invoke(compile=True) + try: + co = compile("""if 1: + def f(): + return 1 + \n""", '', 'exec') + d = {} + exec co in d + assert py.code.Source(d['f']) + finally: + py.magic.revoke(compile=True) + recwarn.pop(DeprecationWarning) + +def test_patch_revert(recwarn): + class a: + pass + py.test.raises(AttributeError, "py.magic.patch(a, 'i', 42)") + + a.i = 42 + py.magic.patch(a, 'i', 23) + assert a.i == 23 + recwarn.pop(DeprecationWarning) + py.magic.revert(a, 'i') + assert a.i == 42 + recwarn.pop(DeprecationWarning) + +def test_double_patch(recwarn): + class a: + i = 42 + assert py.magic.patch(a, 'i', 2) == 42 + recwarn.pop(DeprecationWarning) + assert py.magic.patch(a, 'i', 3) == 2 + assert a.i == 3 + assert py.magic.revert(a, 'i') == 3 + recwarn.pop(DeprecationWarning) + assert a.i == 2 + assert py.magic.revert(a, 'i') == 2 + assert a.i == 42 + +def test_valueerror(recwarn): + class a: + i = 2 + pass + py.test.raises(ValueError, "py.magic.revert(a, 'i')") + recwarn.pop(DeprecationWarning) diff --git a/py/magic/assertion.py b/py/magic/assertion.py deleted file mode 100644 index 31eb13c95..000000000 --- a/py/magic/assertion.py +++ /dev/null @@ -1,38 +0,0 @@ -import __builtin__, sys -import py -from py.__.magic import exprinfo - -BuiltinAssertionError = __builtin__.AssertionError - -class AssertionError(BuiltinAssertionError): - def __init__(self, *args): - BuiltinAssertionError.__init__(self, *args) - if args: - try: - self.msg = str(args[0]) - except (KeyboardInterrupt, SystemExit): - raise - except: - self.msg = "<[broken __repr__] %s at %0xd>" %( - args[0].__class__, id(args[0])) - - else: - f = sys._getframe(1) - try: - source = py.code.Frame(f).statement - source = str(source.deindent()).strip() - except py.error.ENOENT: - source = None - # this can also occur during reinterpretation, when the - # co_filename is set to "". - if source: - self.msg = exprinfo.interpret(source, f, should_fail=True) - if not self.args: - self.args = (self.msg,) - else: - self.msg = None - -def invoke(): - py.magic.patch(__builtin__, 'AssertionError', AssertionError) -def revoke(): - py.magic.revert(__builtin__, 'AssertionError') diff --git a/py/magic/invoke.py b/py/magic/invoke.py deleted file mode 100644 index 809f6b4a7..000000000 --- a/py/magic/invoke.py +++ /dev/null @@ -1,24 +0,0 @@ -import py -import __builtin__ as cpy_builtin - -def invoke(assertion=False, compile=False): - """ invoke magic, currently you can specify: - - assertion patches the builtin AssertionError to try to give - more meaningful AssertionErrors, which by means - of deploying a mini-interpreter constructs - a useful error message. - """ - if assertion: - from py.__.magic import assertion - assertion.invoke() - if compile: - py.magic.patch(cpy_builtin, 'compile', py.code.compile ) - -def revoke(assertion=False, compile=False): - """ revoke previously invoked magic (see invoke()).""" - if assertion: - from py.__.magic import assertion - assertion.revoke() - if compile: - py.magic.revert(cpy_builtin, 'compile') diff --git a/py/magic/patch.py b/py/magic/patch.py deleted file mode 100644 index 31e146855..000000000 --- a/py/magic/patch.py +++ /dev/null @@ -1,26 +0,0 @@ - -patched = {} - -def patch(namespace, name, value): - """ rebind the 'name' on the 'namespace' to the 'value', - possibly and remember the original value. Multiple - invocations to the same namespace/name pair will - remember a list of old values. - """ - nref = (namespace, name) - orig = getattr(namespace, name) - patched.setdefault(nref, []).append(orig) - setattr(namespace, name, value) - return orig - -def revert(namespace, name): - """ revert to the orginal value the last patch modified. - Raise ValueError if no such original value exists. - """ - nref = (namespace, name) - if nref not in patched or not patched[nref]: - raise ValueError, "No original value stored for %s.%s" % nref - current = getattr(namespace, name) - orig = patched[nref].pop() - setattr(namespace, name, orig) - return current diff --git a/py/magic/testing/test_assertion.py b/py/magic/testing/test_assertion.py deleted file mode 100644 index b3b337175..000000000 --- a/py/magic/testing/test_assertion.py +++ /dev/null @@ -1,106 +0,0 @@ -import py - -def setup_module(mod): - py.magic.invoke(assertion=1) - -def teardown_module(mod): - py.magic.revoke(assertion=1) - -def f(): - return 2 - -def test_assert(): - try: - assert f() == 3 - except AssertionError, e: - s = str(e) - assert s.startswith('assert 2 == 3\n') - -def test_assert_with_explicit_message(): - try: - assert f() == 3, "hello" - except AssertionError, e: - assert e.msg == 'hello' - -def test_assert_within_finally(): - class A: - def f(): - pass - excinfo = py.test.raises(TypeError, """ - try: - A().f() - finally: - i = 42 - """) - s = excinfo.exconly() - assert s.find("takes no argument") != -1 - - #def g(): - # A.f() - #excinfo = getexcinfo(TypeError, g) - #msg = getmsg(excinfo) - #assert msg.find("must be called with A") != -1 - - -def test_assert_multiline_1(): - try: - assert (f() == - 3) - except AssertionError, e: - s = str(e) - assert s.startswith('assert 2 == 3\n') - -def test_assert_multiline_2(): - try: - assert (f() == (4, - 3)[-1]) - except AssertionError, e: - s = str(e) - assert s.startswith('assert 2 ==') - -def test_assert_non_string_message(): - class A: - def __str__(self): - return "hello" - try: - assert 0 == 1, A() - except AssertionError, e: - assert e.msg == "hello" - - -# These tests should both fail, but should fail nicely... -class WeirdRepr: - def __repr__(self): - return '' - -def bug_test_assert_repr(): - v = WeirdRepr() - try: - assert v == 1 - except AssertionError, e: - assert e.msg.find('WeirdRepr') != -1 - assert e.msg.find('second line') != -1 - assert 0 - -def test_assert_non_string(): - try: - assert 0, ['list'] - except AssertionError, e: - assert e.msg.find("list") != -1 - -def test_assert_implicit_multiline(): - try: - x = [1,2,3] - assert x != [1, - 2, 3] - except AssertionError, e: - assert e.msg.find('assert [1, 2, 3] !=') != -1 - - -def test_assert_with_brokenrepr_arg(): - class BrokenRepr: - def __repr__(self): 0 / 0 - e = AssertionError(BrokenRepr()) - if e.msg.find("broken __repr__") == -1: - py.test.fail("broken __repr__ not handle correctly") - diff --git a/py/magic/testing/test_exprinfo.py b/py/magic/testing/test_exprinfo.py deleted file mode 100644 index 666460e83..000000000 --- a/py/magic/testing/test_exprinfo.py +++ /dev/null @@ -1,156 +0,0 @@ - -import sys -import py -from py.__.magic.exprinfo import getmsg, interpret - -def getexcinfo(exc, obj, *args, **kwargs): - try: - obj(*args, **kwargs) - except KeyboardInterrupt: - raise - except exc: - return sys.exc_info() - else: - raise AssertionError, "%r(*%r, **%r) did not raise" %( - obj, args, kwargs) - -def test_assert_exprinfo(): - def g(): - a = 1 - b = 2 - assert a == b - excinfo = getexcinfo(AssertionError, g) - msg = getmsg(excinfo) - assert msg == 'assert 1 == 2' - -def test_nested_scopes(): - def g(): - a = 1 - def h(): - return a - b = 2 - assert h() == b - excinfo = getexcinfo(AssertionError, g) - msg = getmsg(excinfo) - assert msg.startswith('assert 1 == 2\n + where 1 = ') - -def test_nested_scopes_2(): - a = 1 - def g(): - b = 2 - assert a == b - excinfo = getexcinfo(AssertionError, g) - msg = getmsg(excinfo) - assert msg == 'assert 1 == 2' - -def test_assert_func_argument_type_error(): - def f (): - pass - def g(): - f(1) - excinfo = getexcinfo(TypeError, g) - msg = getmsg(excinfo) - assert msg.find("takes no argument") != -1 - - class A: - def f(): - pass - def g(): - A().f() - excinfo = getexcinfo(TypeError, g) - msg = getmsg(excinfo) - assert msg.find("takes no argument") != -1 - - def g(): - A.f() - excinfo = getexcinfo(TypeError, g) - msg = getmsg(excinfo) - assert msg.find("must be called with A") != -1 - -def global_f(u=6, v=7): - return u*v - -def test_exprinfo_funccall(): - def g(): - assert global_f() == 43 - excinfo = getexcinfo(AssertionError, g) - msg = getmsg(excinfo) - assert msg == 'assert 42 == 43\n + where 42 = global_f()' - -def test_exprinfo_funccall_keywords(): - def g(): - assert global_f(v=11) == 67 - excinfo = getexcinfo(AssertionError, g) - msg = getmsg(excinfo) - assert msg == 'assert 66 == 67\n + where 66 = global_f(v=11)' - -def test_interpretable_escapes_newlines(): - class X(object): - def __repr__(self): - return '1\n2' - def g(): - assert X() == 'XXX' - - excinfo = getexcinfo(AssertionError, g) - msg = getmsg(excinfo) - assert msg == "assert 1\\n2 == 'XXX'\n + where 1\\n2 = ()" - -def test_keyboard_interrupt(): - # XXX this test is slightly strange because it is not - # clear that "interpret" should execute "raise" statements - # ... but it apparently currently does and it's nice to - # exercise the code because the exprinfo-machinery is - # not much executed when all tests pass ... - - class DummyCode: - co_filename = 'dummy' - co_firstlineno = 0 - co_name = 'dummy' - class DummyFrame: - f_globals = f_locals = {} - f_code = DummyCode - f_lineno = 0 - - for exstr in "SystemExit", "KeyboardInterrupt", "MemoryError": - ex = eval(exstr) - try: - interpret("raise %s" % exstr, py.code.Frame(DummyFrame)) - except ex: - pass - else: - raise AssertionError, "ex %s didn't pass through" %(exstr, ) - -def test_inconsistent_assert_result(testdir): - p = testdir.makepyfile(""" - def test_func(): - def f(l=[1,0]): - return l.pop() - assert f() - """) - result = testdir.runpytest(p) - s = result.stdout.str() - assert s.find("re-run") != -1 - -def test_twoarg_comparison_does_not_call_nonzero(): - # this arises e.g. in numpy array comparisons - class X(object): - def __eq__(self, other): - return self - - def __nonzero__(self): - raise ValueError - - def all(self): - return False - - def f(): - a = X() - b = X() - assert (a == b).all() - - excinfo = getexcinfo(AssertionError, f) - msg = getmsg(excinfo) - print msg - assert "re-run" not in msg - assert "ValueError" not in msg - diff --git a/py/magic/testing/test_invoke.py b/py/magic/testing/test_invoke.py deleted file mode 100644 index 107c33c1e..000000000 --- a/py/magic/testing/test_invoke.py +++ /dev/null @@ -1,29 +0,0 @@ -import __builtin__ as bltin -import py -import inspect - -def check_assertion(): - excinfo = py.test.raises(AssertionError, "assert 1 == 2") - assert excinfo.exconly(tryshort=True) == "assert 1 == 2" - -def test_invoke_assertion(): - py.magic.invoke(assertion=True) - try: - check_assertion() - finally: - py.magic.revoke(assertion=True) - -def test_invoke_compile(): - py.magic.invoke(compile=True) - try: - co = compile("""if 1: - def f(): - return 1 - \n""", '', 'exec') - d = {} - exec co in d - assert py.code.Source(d['f']) - finally: - py.magic.revoke(compile=True) - - diff --git a/py/magic/testing/test_patch.py b/py/magic/testing/test_patch.py deleted file mode 100644 index 888c016b3..000000000 --- a/py/magic/testing/test_patch.py +++ /dev/null @@ -1,31 +0,0 @@ -from py.test import raises -from py.magic import patch, revert - -def test_patch_revert(): - class a: - pass - raises(AttributeError, "patch(a, 'i', 42)") - - a.i = 42 - patch(a, 'i', 23) - assert a.i == 23 - revert(a, 'i') - assert a.i == 42 - -def test_double_patch(): - class a: - i = 42 - assert patch(a, 'i', 2) == 42 - assert patch(a, 'i', 3) == 2 - assert a.i == 3 - assert revert(a, 'i') == 3 - assert a.i == 2 - assert revert(a, 'i') == 2 - assert a.i == 42 - -def test_valueerror(): - class a: - i = 2 - pass - raises(ValueError, "revert(a, 'i')") - diff --git a/py/magic/testing/test_viewtype.py b/py/magic/testing/test_viewtype.py deleted file mode 100644 index 0b82cd8cc..000000000 --- a/py/magic/testing/test_viewtype.py +++ /dev/null @@ -1,55 +0,0 @@ -from py.__.magic.viewtype import View - -def test_class_dispatch(): - ### Use a custom class hierarchy with existing instances - - class Picklable(View): - pass - - class Simple(Picklable): - __view__ = object - def pickle(self): - return repr(self.__obj__) - - class Seq(Picklable): - __view__ = list, tuple, dict - def pickle(self): - return ';'.join([Picklable(item).pickle() for item in self.__obj__]) - - class Dict(Seq): - __view__ = dict - def pickle(self): - return Seq.pickle(self) + '!' + Seq(self.values()).pickle() - - assert Picklable(123).pickle() == '123' - assert Picklable([1,[2,3],4]).pickle() == '1;2;3;4' - assert Picklable({1:2}).pickle() == '1!2' - - -def test_custom_class_hierarchy(): - ### Use a custom class hierarchy based on attributes of existing instances - - class Operation: - "Existing class that I don't want to change." - def __init__(self, opname, *args): - self.opname = opname - self.args = args - - existing = [Operation('+', 4, 5), - Operation('getitem', '', 'join'), - Operation('setattr', 'x', 'y', 3), - Operation('-', 12, 1)] - - class PyOp(View): - def __viewkey__(self): - return self.opname - def generate(self): - return '%s(%s)' % (self.opname, ', '.join(map(repr, self.args))) - - class PyBinaryOp(PyOp): - __view__ = ('+', '-', '*', '/') - def generate(self): - return '%s %s %s' % (self.args[0], self.opname, self.args[1]) - - codelines = [PyOp(op).generate() for op in existing] - assert codelines == ["4 + 5", "getitem('', 'join')", "setattr('x', 'y', 3)", "12 - 1"] diff --git a/py/magic/viewtype.py b/py/magic/viewtype.py deleted file mode 100644 index af0552799..000000000 --- a/py/magic/viewtype.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -The View base class for view-based programming. - -A view of an object is an extension of this existing object. -This is useful to *locally* add methods or even attributes to objects -that you have obtained from elsewhere. -""" -from __future__ import generators -import inspect - -class View(object): - """View base class. - - If C is a subclass of View, then C(x) creates a proxy object around - the object x. The actual class of the proxy is not C in general, - but a *subclass* of C determined by the rules below. To avoid confusion - we call view class the class of the proxy (a subclass of C, so of View) - and object class the class of x. - - Attributes and methods not found in the proxy are automatically read on x. - Other operations like setting attributes are performed on the proxy, as - determined by its view class. The object x is available from the proxy - as its __obj__ attribute. - - The view class selection is determined by the __view__ tuples and the - optional __viewkey__ method. By default, the selected view class is the - most specific subclass of C whose __view__ mentions the class of x. - If no such subclass is found, the search proceeds with the parent - object classes. For example, C(True) will first look for a subclass - of C with __view__ = (..., bool, ...) and only if it doesn't find any - look for one with __view__ = (..., int, ...), and then ..., object,... - If everything fails the class C itself is considered to be the default. - - Alternatively, the view class selection can be driven by another aspect - of the object x, instead of the class of x, by overriding __viewkey__. - See last example at the end of this module. - """ - - _viewcache = {} - __view__ = () - - def __new__(rootclass, obj, *args, **kwds): - self = object.__new__(rootclass) - self.__obj__ = obj - self.__rootclass__ = rootclass - key = self.__viewkey__() - try: - self.__class__ = self._viewcache[key] - except KeyError: - self.__class__ = self._selectsubclass(key) - return self - - def __getattr__(self, attr): - # attributes not found in the normal hierarchy rooted on View - # are looked up in the object's real class - return getattr(self.__obj__, attr) - - def __viewkey__(self): - return self.__obj__.__class__ - - def __matchkey__(self, key, subclasses): - if inspect.isclass(key): - keys = inspect.getmro(key) - else: - keys = [key] - for key in keys: - result = [C for C in subclasses if key in C.__view__] - if result: - return result - return [] - - def _selectsubclass(self, key): - subclasses = list(enumsubclasses(self.__rootclass__)) - for C in subclasses: - if not isinstance(C.__view__, tuple): - C.__view__ = (C.__view__,) - choices = self.__matchkey__(key, subclasses) - if not choices: - return self.__rootclass__ - elif len(choices) == 1: - return choices[0] - else: - # combine the multiple choices - return type('?', tuple(choices), {}) - - def __repr__(self): - return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__) - - -def enumsubclasses(cls): - for subcls in cls.__subclasses__(): - for subsubclass in enumsubclasses(subcls): - yield subsubclass - yield cls diff --git a/py/test/cmdline.py b/py/test/cmdline.py index 5b2a86743..f34f711f5 100644 --- a/py/test/cmdline.py +++ b/py/test/cmdline.py @@ -5,7 +5,6 @@ import py # def main(args=None): - warn_about_missing_assertion() if args is None: args = py.std.sys.argv[1:] config = py.test.config @@ -20,11 +19,3 @@ def main(args=None): py.std.sys.stderr.write("ERROR: %s\n" %(e.args[0],)) raise SystemExit(3) -def warn_about_missing_assertion(): - try: - assert False - except AssertionError: - pass - else: - py.std.warnings.warn("Assertions are turned off!" - " (are you using python -O?)") diff --git a/py/test/defaultconftest.py b/py/test/defaultconftest.py index a44d7ab09..4ebeaa4a4 100644 --- a/py/test/defaultconftest.py +++ b/py/test/defaultconftest.py @@ -10,5 +10,5 @@ Generator = py.test.collect.Generator Function = py.test.collect.Function Instance = py.test.collect.Instance -pytest_plugins = "default runner capture terminal keyword xfail tmpdir execnetcleanup monkeypatch recwarn pdb pastebin unittest helpconfig nose".split() +pytest_plugins = "default runner capture terminal keyword xfail tmpdir execnetcleanup monkeypatch recwarn pdb pastebin unittest helpconfig nose assertion".split() diff --git a/py/test/plugin/pytest_assertion.py b/py/test/plugin/pytest_assertion.py new file mode 100644 index 000000000..eb3cdbde5 --- /dev/null +++ b/py/test/plugin/pytest_assertion.py @@ -0,0 +1,68 @@ +import py + +def pytest_addoption(parser): + group = parser.getgroup("debugconfig") + group._addoption('--no-assert', action="store_true", default=False, + dest="noassert", + help="disable python assert expression reinterpretation."), + +def pytest_configure(config): + if not config.getvalue("noassert"): + warn_about_missing_assertion() + config._oldassertion = py.std.__builtin__.AssertionError + py.std.__builtin__.AssertionError = py.code._AssertionError + +def pytest_unconfigure(config): + if hasattr(config, '_oldassertion'): + py.std.__builtin__.AssertionError = config._oldassertion + del config._oldassertion + +def warn_about_missing_assertion(): + try: + assert False + except AssertionError: + pass + else: + py.std.warnings.warn("Assertions are turned off!" + " (are you using python -O?)") + +def test_functional(testdir): + testdir.makepyfile(""" + def test_hello(): + x = 3 + assert x == 4 + """) + result = testdir.runpytest() + assert "3 == 4" in result.stdout.str() + result = testdir.runpytest("--no-assert") + assert "3 == 4" not in result.stdout.str() + +def test_traceback_failure(testdir): + p1 = testdir.makepyfile(""" + def g(): + return 2 + def f(x): + assert x == g() + def test_onefails(): + f(3) + """) + result = testdir.runpytest(p1) + result.stdout.fnmatch_lines([ + "*test_traceback_failure.py F", + "====* FAILURES *====", + "____*____", + "", + " def test_onefails():", + "> f(3)", + "", + "*test_*.py:6: ", + "_ _ _ *", + #"", + " def f(x):", + "> assert x == g()", + "E assert 3 == 2", + "E + where 2 = g()", + "", + "*test_traceback_failure.py:4: AssertionError" + ]) + diff --git a/py/test/plugin/test_pytest_terminal.py b/py/test/plugin/test_pytest_terminal.py index f80c10965..0a9758ffc 100644 --- a/py/test/plugin/test_pytest_terminal.py +++ b/py/test/plugin/test_pytest_terminal.py @@ -564,36 +564,6 @@ class TestTerminalFunctional: "=* 1 passed in *.[0-9][0-9] seconds *=", ]) - def test_traceback_failure(self, testdir): - p1 = testdir.makepyfile(""" - def g(): - return 2 - def f(x): - assert x == g() - def test_onefails(): - f(3) - """) - result = testdir.runpytest(p1) - result.stdout.fnmatch_lines([ - "*test_traceback_failure.py F", - "====* FAILURES *====", - "____*____", - "", - " def test_onefails():", - "> f(3)", - "", - "*test_*.py:6: ", - "_ _ _ *", - #"", - " def f(x):", - "> assert x == g()", - "E assert 3 == 2", - "E + where 2 = g()", - "", - "*test_traceback_failure.py:4: AssertionError" - ]) - - def test_showlocals(self, testdir): p1 = testdir.makepyfile(""" def test_showlocals(): diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 4df59b1ee..6de661b73 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -183,21 +183,13 @@ class Module(py.test.collect.File, PyCollectorMixin): def setup(self): if getattr(self.obj, 'disabled', 0): py.test.skip("%r is disabled" %(self.obj,)) - if not self.config.option.nomagic: - #print "*" * 20, "INVOKE assertion", self - py.magic.invoke(assertion=1) mod = self.obj - #self.config.pluginmanager.register(mod) if hasattr(mod, 'setup_module'): self.obj.setup_module(mod) def teardown(self): if hasattr(self.obj, 'teardown_module'): self.obj.teardown_module(self.obj) - if not self.config.option.nomagic: - #print "*" * 20, "revoke assertion", self - py.magic.revoke(assertion=1) - #self.config.pluginmanager.unregister(self.obj) class Class(PyCollectorMixin, py.test.collect.Collector): diff --git a/py/test/testing/test_pycollect.py b/py/test/testing/test_pycollect.py index dcec63e98..ce74bd808 100644 --- a/py/test/testing/test_pycollect.py +++ b/py/test/testing/test_pycollect.py @@ -22,19 +22,6 @@ class TestModule: py.test.raises(SyntaxError, modcol.collect) py.test.raises(SyntaxError, modcol.run) - def test_module_assertion_setup(self, testdir, monkeypatch): - modcol = testdir.getmodulecol("pass") - from py.__.magic import assertion - l = [] - monkeypatch.setattr(assertion, "invoke", lambda: l.append(None)) - modcol.setup() - x = l.pop() - assert x is None - monkeypatch.setattr(assertion, "revoke", lambda: l.append(None)) - modcol.teardown() - x = l.pop() - assert x is None - def test_module_considers_pluginmanager_at_import(self, testdir): modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") py.test.raises(ImportError, "modcol.obj")