From 7d66e4eae1cc5989df07e140f2ff6949416c2c18 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Oct 2016 20:47:44 -0300 Subject: [PATCH 1/2] Display full traceback from Import errors when collecting test modules Fix #1976 --- CHANGELOG.rst | 9 ++++++++- _pytest/python.py | 13 ++++++++----- testing/acceptance_test.py | 2 +- testing/python/collect.py | 22 +++++++++++++++++++++- testing/test_collection.py | 11 ----------- testing/test_terminal.py | 2 +- 6 files changed, 39 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6abd5006a..96a257781 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,13 +3,20 @@ * -* +* Import errors when collecting test modules now display the full traceback (`#1976`_). + Thanks `@cwitty`_ for the report and `@nicoddemus`_ for the PR. * * +.. _@cwitty: https://github.com/cwitty + +.. _#1976: https://github.com/pytest-dev/pytest/issues/1976 + + + 3.0.3 ===== diff --git a/_pytest/python.py b/_pytest/python.py index 548d7cfa5..74bb4de43 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -424,12 +424,15 @@ class Module(pytest.File, PyCollector): % e.args ) except ImportError: - exc_class, exc, _ = sys.exc_info() + import traceback + stream = py.io.TextIO() + traceback.print_exc(file=stream) + formatted_tb = stream.getvalue() raise self.CollectError( - "ImportError while importing test module '%s'.\n" - "Original error message:\n'%s'\n" - "Make sure your test modules/packages have valid Python names." - % (self.fspath, exc or exc_class) + "ImportError while importing test module '{fspath}'.\n" + "Hint: make sure your test modules/packages have valid Python names.\n" + "Original traceback:\n" + "{traceback}".format(fspath=self.fspath, traceback=formatted_tb) ) except _pytest.runner.Skipped as e: if e.allow_module_level: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index b03f7fe4c..88e3fa449 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -120,7 +120,7 @@ class TestGeneralUsage: result.stdout.fnmatch_lines([ #XXX on jython this fails: "> import import_fails", "ImportError while importing test module*", - "'No module named *does_not_work*", + "*No module named *does_not_work*", ]) assert result.ret == 2 diff --git a/testing/python/collect.py b/testing/python/collect.py index 843f26a73..04ce53a54 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -68,9 +68,29 @@ class TestModule: result = testdir.runpytest("-rw") result.stdout.fnmatch_lines([ "ImportError while importing test module*test_one.part1*", - "Make sure your test modules/packages have valid Python names.", + "Hint: make sure your test modules/packages have valid Python names.", ]) + def test_show_full_traceback_import_error(self, testdir): + """Import errors when collecting modules should display the full traceback (#1976).""" + testdir.makepyfile( + foo_traceback_import_error=""" + from bar_traceback_import_error import NOT_AVAILABLE + """, + bar_traceback_import_error="", + ) + testdir.makepyfile(""" + import foo_traceback_import_error + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "ImportError while importing test module*", + "Original traceback:", + "*from bar_traceback_import_error import NOT_AVAILABLE", + "*cannot import name *NOT_AVAILABLE*", + ]) + assert result.ret == 2 + class TestClass: def test_class_with_init_warning(self, testdir): diff --git a/testing/test_collection.py b/testing/test_collection.py index 8e44ba55d..71a64c3c9 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -172,17 +172,6 @@ class TestCollectPluginHookRelay: assert "world" in wascalled class TestPrunetraceback: - def test_collection_error(self, testdir): - p = testdir.makepyfile(""" - import not_exists - """) - result = testdir.runpytest(p) - assert "__import__" not in result.stdout.str(), "too long traceback" - result.stdout.fnmatch_lines([ - "*ERROR collecting*", - "ImportError while importing test module*", - "'No module named *not_exists*", - ]) def test_custom_repr_failure(self, testdir): p = testdir.makepyfile(""" diff --git a/testing/test_terminal.py b/testing/test_terminal.py index fc6f3b7b1..66214c80e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -667,7 +667,7 @@ class TestGenericReporting: result = testdir.runpytest(*option.args) result.stdout.fnmatch_lines([ "ImportError while importing*", - "'No module named *xyz*", + "*No module named *xyz*", "*1 error*", ]) From a1d446b8e809f1e6488ce183ace93ab6693f827c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Oct 2016 21:46:44 -0300 Subject: [PATCH 2/2] Display traceback from Import errors using pytest's short representation Also omit pytest's own modules and internal libraries (py and pluggy) in low verbosity Fix #1976 --- _pytest/python.py | 21 ++++++++++++++------- testing/python/collect.py | 21 +++++++++++++++++---- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index 74bb4de43..62d2896ea 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -22,11 +22,16 @@ from _pytest.compat import ( getlocation, enum, ) -cutdir2 = py.path.local(_pytest.__file__).dirpath() cutdir1 = py.path.local(pluggy.__file__.rstrip("oc")) +cutdir2 = py.path.local(_pytest.__file__).dirpath() +cutdir3 = py.path.local(py.__file__).dirpath() def filter_traceback(entry): + """Return True if a TracebackEntry instance should be removed from tracebacks: + * dynamically generated code (no code to show up for it); + * internal traceback from pytest or its internal libraries, py and pluggy. + """ # entry.path might sometimes return a str object when the entry # points to dynamically generated code # see https://bitbucket.org/pytest-dev/py/issues/71 @@ -37,7 +42,7 @@ def filter_traceback(entry): # entry.path might point to an inexisting file, in which case it will # alsso return a str object. see #1133 p = py.path.local(entry.path) - return p != cutdir1 and not p.relto(cutdir2) + return p != cutdir1 and not p.relto(cutdir2) and not p.relto(cutdir3) @@ -424,14 +429,16 @@ class Module(pytest.File, PyCollector): % e.args ) except ImportError: - import traceback - stream = py.io.TextIO() - traceback.print_exc(file=stream) - formatted_tb = stream.getvalue() + from _pytest._code.code import ExceptionInfo + exc_info = ExceptionInfo() + if self.config.getoption('verbose') < 2: + exc_info.traceback = exc_info.traceback.filter(filter_traceback) + exc_repr = exc_info.getrepr(style='short') if exc_info.traceback else exc_info.exconly() + formatted_tb = py._builtin._totext(exc_repr) raise self.CollectError( "ImportError while importing test module '{fspath}'.\n" "Hint: make sure your test modules/packages have valid Python names.\n" - "Original traceback:\n" + "Traceback:\n" "{traceback}".format(fspath=self.fspath, traceback=formatted_tb) ) except _pytest.runner.Skipped as e: diff --git a/testing/python/collect.py b/testing/python/collect.py index 04ce53a54..2913b11a4 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import os import sys from textwrap import dedent @@ -71,8 +72,12 @@ class TestModule: "Hint: make sure your test modules/packages have valid Python names.", ]) - def test_show_full_traceback_import_error(self, testdir): - """Import errors when collecting modules should display the full traceback (#1976).""" + @pytest.mark.parametrize('verbose', [0, 1, 2]) + def test_show_traceback_import_error(self, testdir, verbose): + """Import errors when collecting modules should display the traceback (#1976). + + With low verbosity we omit pytest and internal modules, otherwise show all traceback entries. + """ testdir.makepyfile( foo_traceback_import_error=""" from bar_traceback_import_error import NOT_AVAILABLE @@ -82,15 +87,23 @@ class TestModule: testdir.makepyfile(""" import foo_traceback_import_error """) - result = testdir.runpytest() + args = ('-v',) * verbose + result = testdir.runpytest(*args) result.stdout.fnmatch_lines([ "ImportError while importing test module*", - "Original traceback:", + "Traceback:", "*from bar_traceback_import_error import NOT_AVAILABLE", "*cannot import name *NOT_AVAILABLE*", ]) assert result.ret == 2 + stdout = result.stdout.str() + for name in ('_pytest', os.path.join('py', '_path')): + if verbose == 2: + assert name in stdout + else: + assert name not in stdout + class TestClass: def test_class_with_init_warning(self, testdir):