[py-svn] commit/pytest: gutworth: rewrite test modules on import

Bitbucket commits-noreply at bitbucket.org
Wed Jun 29 04:14:37 CEST 2011


1 new changeset in pytest:

http://bitbucket.org/hpk42/pytest/changeset/2f337b6a5d09/
changeset:   2f337b6a5d09
user:        gutworth
date:        2011-06-29 04:13:12
summary:     rewrite test modules on import
affected #:  4 files (5.8 KB)

--- a/_pytest/assertion/__init__.py	Tue Jun 28 21:11:56 2011 -0500
+++ b/_pytest/assertion/__init__.py	Tue Jun 28 21:13:12 2011 -0500
@@ -2,20 +2,12 @@
 support for presenting detailed information in failing assertions.
 """
 import py
-import imp
-import marshal
-import struct
 import sys
 import pytest
 from _pytest.monkeypatch import monkeypatch
-from _pytest.assertion import reinterpret, util
+from _pytest.assertion import util
 
-try:
-    from _pytest.assertion.rewrite import rewrite_asserts
-except ImportError:
-    rewrite_asserts = None
-else:
-    import ast
+REWRITING_AVAILABLE = "_ast" in sys.builtin_module_names
 
 def pytest_addoption(parser):
     group = parser.getgroup("debugconfig")
@@ -38,9 +30,9 @@
     def __init__(self, config, mode):
         self.mode = mode
         self.trace = config.trace.root.get("assertion")
+        self.pycs = []
 
 def pytest_configure(config):
-    warn_about_missing_assertion()
     mode = config.getvalue("assertmode")
     if config.getvalue("noassert") or config.getvalue("nomagic"):
         if mode not in ("off", "default"):
@@ -48,7 +40,10 @@
         mode = "off"
     elif mode == "default":
         mode = "on"
+    if mode == "on" and not REWRITING_AVAILABLE:
+        mode = "old"
     if mode != "off":
+        _load_modules(mode)
         def callbinrepr(op, left, right):
             hook_result = config.hook.pytest_assertrepr_compare(
                 config=config, op=op, left=left, right=right)
@@ -60,69 +55,55 @@
         m.setattr(py.builtin.builtins, 'AssertionError',
                   reinterpret.AssertionError)
         m.setattr(util, '_reprcompare', callbinrepr)
-    if mode == "on" and rewrite_asserts is None:
-        mode = "old"
+    hook = None
+    if mode == "on":
+        hook = rewrite.AssertionRewritingHook()
+        sys.meta_path.append(hook)
+    warn_about_missing_assertion(mode)
     config._assertstate = AssertionState(config, mode)
+    config._assertstate.hook = hook
     config._assertstate.trace("configured with mode set to %r" % (mode,))
 
-def _write_pyc(co, source_path):
-    if hasattr(imp, "cache_from_source"):
-        # Handle PEP 3147 pycs.
-        pyc = py.path.local(imp.cache_from_source(str(source_path)))
-        pyc.ensure()
-    else:
-        pyc = source_path + "c"
-    mtime = int(source_path.mtime())
-    fp = pyc.open("wb")
-    try:
-        fp.write(imp.get_magic())
-        fp.write(struct.pack("<l", mtime))
-        marshal.dump(co, fp)
-    finally:
-        fp.close()
-    return pyc
+def pytest_unconfigure(config):
+    if config._assertstate.mode == "on":
+        rewrite._drain_pycs(config._assertstate)
+    hook = config._assertstate.hook
+    if hook is not None:
+        sys.meta_path.remove(hook)
 
-def before_module_import(mod):
-    if mod.config._assertstate.mode != "on":
-        return
-    # Some deep magic: load the source, rewrite the asserts, and write a
-    # fake pyc, so that it'll be loaded when the module is imported.
-    source = mod.fspath.read()
-    try:
-        tree = ast.parse(source)
-    except SyntaxError:
-        # Let this pop up again in the real import.
-        mod.config._assertstate.trace("failed to parse: %r" % (mod.fspath,))
-        return
-    rewrite_asserts(tree)
-    try:
-        co = compile(tree, str(mod.fspath), "exec")
-    except SyntaxError:
-        # It's possible that this error is from some bug in the assertion
-        # rewriting, but I don't know of a fast way to tell.
-        mod.config._assertstate.trace("failed to compile: %r" % (mod.fspath,))
-        return
-    mod._pyc = _write_pyc(co, mod.fspath)
-    mod.config._assertstate.trace("wrote pyc: %r" % (mod._pyc,))
+def pytest_sessionstart(session):
+    hook = session.config._assertstate.hook
+    if hook is not None:
+        hook.set_session(session)
 
-def after_module_import(mod):
-    if not hasattr(mod, "_pyc"):
-        return
-    state = mod.config._assertstate
-    try:
-        mod._pyc.remove()
-    except py.error.ENOENT:
-        state.trace("couldn't find pyc: %r" % (mod._pyc,))
-    else:
-        state.trace("removed pyc: %r" % (mod._pyc,))
+def pytest_sessionfinish(session):
+    if session.config._assertstate.mode == "on":
+        rewrite._drain_pycs(session.config._assertstate)
+    hook = session.config._assertstate.hook
+    if hook is not None:
+        hook.session = None
 
-def warn_about_missing_assertion():
+def _load_modules(mode):
+    """Lazily import assertion related code."""
+    global rewrite, reinterpret
+    from _pytest.assertion import reinterpret
+    if mode == "on":
+        from _pytest.assertion import rewrite
+
+def warn_about_missing_assertion(mode):
     try:
         assert False
     except AssertionError:
         pass
     else:
-        sys.stderr.write("WARNING: failing tests may report as passing because "
-        "assertions are turned off!  (are you using python -O?)\n")
+        if mode == "on":
+            specifically = ("assertions which are not in test modules "
+                            "will be ignored")
+        else:
+            specifically = "failing tests may report as passing"
+
+        sys.stderr.write("WARNING: " + specifically +
+                        " because assertions are turned off "
+                        "(are you using python -O?)\n")
 
 pytest_assertrepr_compare = util.assertrepr_compare


--- a/_pytest/assertion/rewrite.py	Tue Jun 28 21:11:56 2011 -0500
+++ b/_pytest/assertion/rewrite.py	Tue Jun 28 21:13:12 2011 -0500
@@ -3,12 +3,173 @@
 import ast
 import collections
 import itertools
+import imp
+import marshal
+import os
+import struct
 import sys
 
 import py
 from _pytest.assertion import util
 
 
+# py.test caches rewritten pycs in __pycache__.
+if hasattr(imp, "get_tag"):
+    PYTEST_TAG = imp.get_tag() + "-PYTEST"
+else:
+    ver = sys.version_info
+    PYTEST_TAG = "cpython-" + str(ver[0]) + str(ver[1]) + "-PYTEST"
+    del ver
+
+class AssertionRewritingHook(object):
+    """Import hook which rewrites asserts.
+
+    Note this hook doesn't load modules itself. It uses find_module to write a
+    fake pyc, so the normal import system will find it.
+    """
+
+    def __init__(self):
+        self.session = None
+
+    def set_session(self, session):
+        self.fnpats = session.config.getini("python_files")
+        self.session = session
+
+    def find_module(self, name, path=None):
+        if self.session is None:
+            return None
+        sess = self.session
+        state = sess.config._assertstate
+        names = name.rsplit(".", 1)
+        lastname = names[-1]
+        pth = None
+        if path is not None and len(path) == 1:
+            pth = path[0]
+        if pth is None:
+            try:
+                fd, fn, desc = imp.find_module(lastname, path)
+            except ImportError:
+                return None
+            if fd is not None:
+                fd.close()
+            tp = desc[2]
+            if tp == imp.PY_COMPILED:
+                if hasattr(imp, "source_from_cache"):
+                    fn = imp.source_from_cache(fn)
+                else:
+                    fn = fn[:-1]
+            elif tp != imp.PY_SOURCE:
+                # Don't know what this is.
+                return None
+        else:
+            fn = os.path.join(pth, name + ".py")
+        fn_pypath = py.path.local(fn)
+        # Is this a test file?
+        if not sess.isinitpath(fn):
+            # We have to be very careful here because imports in this code can
+            # trigger a cycle.
+            self.session = None
+            try:
+                for pat in self.fnpats:
+                    if fn_pypath.fnmatch(pat):
+                        break
+                else:
+                    return None
+            finally:
+                self.session = sess
+        # This looks like a test file, so rewrite it. This is the most magical
+        # part of the process: load the source, rewrite the asserts, and write a
+        # fake pyc, so that it'll be loaded when the module is imported. This is
+        # complicated by the fact we cache rewritten pycs.
+        pyc = _compute_pyc_location(fn_pypath)
+        state.pycs.append(pyc)
+        cache_fn = fn_pypath.basename[:-3] + "." + PYTEST_TAG + ".pyc"
+        cache = py.path.local(fn_pypath.dirname).join("__pycache__", cache_fn)
+        if _use_cached_pyc(fn_pypath, cache):
+            state.trace("found cached rewritten pyc for %r" % (fn,))
+            cache.copy(pyc)
+        else:
+            state.trace("rewriting %r" % (fn,))
+            _make_rewritten_pyc(state, fn_pypath, pyc)
+            # Try cache it in the __pycache__ directory.
+            _cache_pyc(state, pyc, cache)
+        return None
+
+def _drain_pycs(state):
+    for pyc in state.pycs:
+        try:
+            pyc.remove()
+        except py.error.ENOENT:
+            state.trace("couldn't find pyc: %r" % (pyc,))
+        else:
+            state.trace("removed pyc: %r" % (pyc,))
+
+def _write_pyc(co, source_path, pyc):
+    mtime = int(source_path.mtime())
+    fp = pyc.open("wb")
+    try:
+        fp.write(imp.get_magic())
+        fp.write(struct.pack("<l", mtime))
+        marshal.dump(co, fp)
+    finally:
+        fp.close()
+
+def _make_rewritten_pyc(state, fn, pyc):
+    try:
+        source = fn.read("rb")
+    except EnvironmentError:
+        return None
+    try:
+        tree = ast.parse(source)
+    except SyntaxError:
+        # Let this pop up again in the real import.
+        state.trace("failed to parse: %r" % (fn,))
+        return None
+    rewrite_asserts(tree)
+    try:
+        co = compile(tree, fn.strpath, "exec")
+    except SyntaxError:
+        # It's possible that this error is from some bug in the
+        # assertion rewriting, but I don't know of a fast way to tell.
+        state.trace("failed to compile: %r" % (fn,))
+        return None
+    _write_pyc(co, fn, pyc)
+
+def _compute_pyc_location(source_path):
+    if hasattr(imp, "cache_from_source"):
+        # Handle PEP 3147 pycs.
+        pyc = py.path.local(imp.cache_from_source(str(source_path)))
+        pyc.ensure()
+    else:
+        pyc = source_path + "c"
+    return pyc
+
+def _use_cached_pyc(source, cache):
+    try:
+        mtime = source.mtime()
+        fp = cache.open("rb")
+        try:
+            data = fp.read(8)
+        finally:
+            fp.close()
+    except EnvironmentError:
+        return False
+    if (len(data) != 8 or
+        data[:4] != imp.get_magic() or
+        struct.unpack("<l", data[4:])[0] != mtime):
+        # Invalid or out of date.
+        return False
+    # The cached pyc exists and is up to date.
+    return True
+
+def _cache_pyc(state, pyc, cache):
+    try:
+        cache.dirpath().ensure(dir=True)
+        pyc.copy(cache)
+    except EnvironmentError:
+        state.trace("failed to cache %r as %r" % (pyc, cache))
+
+
 def rewrite_asserts(mod):
     """Rewrite the assert statements in mod."""
     AssertionRewriter().run(mod)


--- a/_pytest/python.py	Tue Jun 28 21:11:56 2011 -0500
+++ b/_pytest/python.py	Tue Jun 28 21:13:12 2011 -0500
@@ -226,13 +226,8 @@
 
     def _importtestmodule(self):
         # we assume we are only called once per module
-        from _pytest import assertion
-        assertion.before_module_import(self)
         try:
-            try:
-                mod = self.fspath.pyimport(ensuresyspath=True)
-            finally:
-                assertion.after_module_import(self)
+            mod = self.fspath.pyimport(ensuresyspath=True)
         except SyntaxError:
             excinfo = py.code.ExceptionInfo()
             raise self.CollectError(excinfo.getrepr(style="short"))


--- a/testing/test_assertion.py	Tue Jun 28 21:11:56 2011 -0500
+++ b/testing/test_assertion.py	Tue Jun 28 21:13:12 2011 -0500
@@ -250,8 +250,9 @@
     ])
 
 def test_load_fake_pyc(testdir):
-    path = testdir.makepyfile("x = 'hello'")
+    rewrite = pytest.importorskip("_pytest.assertion.rewrite")
+    path = testdir.makepyfile(a_random_module="x = 'hello'")
     co = compile("x = 'bye'", str(path), "exec")
-    plugin._write_pyc(co, path)
+    rewrite._write_pyc(co, path, rewrite._compute_pyc_location(path))
     mod = path.pyimport()
     assert mod.x == "bye"

Repository URL: https://bitbucket.org/hpk42/pytest/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.



More information about the pytest-commit mailing list