From 57bb3c6922ec32dcc7af17029eed357ed0742546 Mon Sep 17 00:00:00 2001 From: Nikolaus Rath Date: Mon, 26 Sep 2016 21:07:46 -0700 Subject: [PATCH 01/14] Improve error message when using pytest.skip at module level As discussed in issue #1959. --- _pytest/python.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index eacea994d..548d7cfa5 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -435,9 +435,9 @@ class Module(pytest.File, PyCollector): if e.allow_module_level: raise raise self.CollectError( - "Using @pytest.skip outside of a test (e.g. as a test " - "function decorator) is not allowed. Use @pytest.mark.skip or " - "@pytest.mark.skipif instead." + "Using pytest.skip outside of a test is not allowed. If you are " + "trying to decorate a test function, use the @pytest.mark.skip " + "or @pytest.mark.skipif decorators instead." ) self.config.pluginmanager.consider_module(mod) return mod From 336d7900c587caa35c0bb9067bf6820fe66a89ea Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 1 Oct 2016 13:38:41 -0300 Subject: [PATCH 02/14] Fix test about pytest.skip message being used at global level Fix #1959 --- testing/test_skipping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 12b18ca33..2e7868d3a 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -967,5 +967,5 @@ def test_module_level_skip_error(testdir): """) result = testdir.runpytest() result.stdout.fnmatch_lines( - "*Using @pytest.skip outside of a test * is not allowed*" + "*Using pytest.skip outside of a test is not allowed*" ) From 7d66e4eae1cc5989df07e140f2ff6949416c2c18 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Oct 2016 20:47:44 -0300 Subject: [PATCH 03/14] 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 04/14] 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): From c93a9e33617a5a5a79c59f8f51b9bf5112fb7690 Mon Sep 17 00:00:00 2001 From: Tom V Date: Tue, 4 Oct 2016 14:41:09 +0100 Subject: [PATCH 05/14] Fix #1981, improve ini-options help text --- _pytest/helpconfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py index 84f419a08..538a763ca 100644 --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -71,8 +71,8 @@ def showhelp(config): tw.write(config._parser.optparser.format_help()) tw.line() tw.line() - tw.line("[pytest] ini-options in the next " - "pytest.ini|tox.ini|setup.cfg file:") + tw.line("[pytest] ini-options in the first " + "pytest.ini|tox.ini|setup.cfg file found:") tw.line() for name in config._parser._ininames: From 50b960c1f065e4ead2343bb266433c6cec594b42 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Oct 2016 12:57:40 -0300 Subject: [PATCH 06/14] Add note about not monkey-patching builtins (#1986) * Add note about not monkey-patching builtins Related to #1985 * Mention -s as well --- doc/en/monkeypatch.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst index e85d94d6d..806e910bd 100644 --- a/doc/en/monkeypatch.rst +++ b/doc/en/monkeypatch.rst @@ -55,6 +55,14 @@ will delete the method ``request.session.Session.request`` so that any attempts within tests to create http requests will fail. +.. note:: + + Be advised that it is not recommended to patch builtin functions such as ``open``, + ``compile``, etc., because it might break pytest's internals. If that's + unavoidable, passing ``--tb=native``, ``--assert=plain`` and ``--capture=no`` might + help althought there's no guarantee. + + Method reference of the monkeypatch fixture ------------------------------------------- From 10433db2258c1461f48b80b0bac5fb4604272bdb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Oct 2016 15:36:38 -0300 Subject: [PATCH 07/14] Mention small doc fixes don't need tests/changelog entries in PR template --- .github/PULL_REQUEST_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 198780a2a..1191fad27 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,6 +3,9 @@ Thanks for submitting a PR, your contribution is really appreciated! Here's a quick checklist that should be present in PRs: - [ ] Target: for bug or doc fixes, target `master`; for new features, target `features`; + +Unless your change is trivial documentation fix (e.g., a typo or reword of a small section) please: + - [ ] Make sure to include one or more tests for your change; - [ ] Add yourself to `AUTHORS`; - [ ] Add a new entry to `CHANGELOG.rst` From 65ebc75ee8d8b16b37eeff4ed725f52bd22578a5 Mon Sep 17 00:00:00 2001 From: Grigorii Eremeev Date: Wed, 5 Oct 2016 20:49:28 +0300 Subject: [PATCH 08/14] Update fixture.rst Removed redundant word --- AUTHORS | 1 + doc/en/fixture.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index d8c431dc0..910a82e49 100644 --- a/AUTHORS +++ b/AUTHORS @@ -59,6 +59,7 @@ Georgy Dyuldin Graham Horler Greg Price Grig Gheorghiu +Grigorii Eremeev (budulianin) Guido Wesdorp Harald Armin Massa Ian Bicking diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index d16f49e12..443e6b6be 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -701,7 +701,7 @@ Using fixtures from classes, modules or projects Sometimes test functions do not directly need access to a fixture object. For example, tests may require to operate with an empty directory as the current working directory but otherwise do not care for the concrete -directory. Here is how you can can use the standard `tempfile +directory. Here is how you can use the standard `tempfile `_ and pytest fixtures to achieve it. We separate the creation of the fixture into a conftest.py file:: From 78eec0d7f8bf177a368575271483ca575c11ad92 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 12 Oct 2016 17:46:47 -0300 Subject: [PATCH 09/14] Handle import errors with non-ascii messages when importing plugins Fix #1998 --- CHANGELOG.rst | 4 +++- _pytest/compat.py | 16 +++++++++++++++- _pytest/config.py | 3 ++- testing/test_pluginmanager.py | 12 +++++++++--- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 96a257781..c03e58a4c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,7 +6,8 @@ * Import errors when collecting test modules now display the full traceback (`#1976`_). Thanks `@cwitty`_ for the report and `@nicoddemus`_ for the PR. -* +* When loading plugins, import errors which contain non-ascii messages are now properly handled in Python 2 (`#1998`_). + Thanks `@nicoddemus`_ for the PR. * @@ -14,6 +15,7 @@ .. _@cwitty: https://github.com/cwitty .. _#1976: https://github.com/pytest-dev/pytest/issues/1976 +.. _#1998: https://github.com/pytest-dev/pytest/issues/1998 diff --git a/_pytest/compat.py b/_pytest/compat.py index 1d8c2f331..51fc3bc5c 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -213,4 +213,18 @@ def _is_unittest_unexpected_success_a_failure(): Changed in version 3.4: Returns False if there were any unexpectedSuccesses from tests marked with the expectedFailure() decorator. """ - return sys.version_info >= (3, 4) \ No newline at end of file + return sys.version_info >= (3, 4) + + +if _PY3: + def safe_str(v): + """returns v as string""" + return str(v) +else: + def safe_str(v): + """returns v as string, converting to ascii if necessary""" + try: + return str(v) + except UnicodeError: + errors = 'replace' + return v.encode('ascii', errors) diff --git a/_pytest/config.py b/_pytest/config.py index 661a8513d..a169a68a2 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -12,6 +12,7 @@ import _pytest._code import _pytest.hookspec # the extension point definitions import _pytest.assertion from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker +from _pytest.compat import safe_str hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") @@ -405,7 +406,7 @@ class PytestPluginManager(PluginManager): try: __import__(importspec) except ImportError as e: - new_exc = ImportError('Error importing plugin "%s": %s' % (modname, e)) + new_exc = ImportError('Error importing plugin "%s": %s' % (modname, safe_str(e.args[0]))) # copy over name and path attributes for attr in ('name', 'path'): if hasattr(e, attr): diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 36847638d..e61c84247 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -1,3 +1,4 @@ +# encoding: UTF-8 import pytest import py import os @@ -179,15 +180,20 @@ def test_default_markers(testdir): ]) -def test_importplugin_issue375(testdir, pytestpm): +def test_importplugin_error_message(testdir, pytestpm): """Don't hide import errors when importing plugins and provide an easy to debug message. + + See #375 and #1998. """ testdir.syspathinsert(testdir.tmpdir) - testdir.makepyfile(qwe="import aaaa") + testdir.makepyfile(qwe=""" + # encoding: UTF-8 + raise ImportError(u'Not possible to import: ☺') + """) with pytest.raises(ImportError) as excinfo: pytestpm.import_plugin("qwe") - expected = '.*Error importing plugin "qwe": No module named \'?aaaa\'?' + expected = '.*Error importing plugin "qwe": Not possible to import: .' assert py.std.re.match(expected, str(excinfo.value)) From bc1f8666aaee6458da9584ecf7ef3cb956b94e47 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 12 Oct 2016 17:58:58 -0300 Subject: [PATCH 10/14] Fix link in CHANGELOG for #1853 --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c03e58a4c..237ea478f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -30,7 +30,7 @@ (``pip install -e``) (`#1934`_). Thanks `@nicoddemus`_ for the PR. -* Fix pkg_resources import error in Jython projects (`#1853`). +* Fix pkg_resources import error in Jython projects (`#1853`_). Thanks `@raquel-ucl`_ for the PR. * Got rid of ``AttributeError: 'Module' object has no attribute '_obj'`` exception @@ -50,6 +50,7 @@ .. _@axil: https://github.com/axil .. _@tgoodlet: https://github.com/tgoodlet +.. _#1853: https://github.com/pytest-dev/pytest/issues/1853 .. _#1905: https://github.com/pytest-dev/pytest/issues/1905 .. _#1934: https://github.com/pytest-dev/pytest/issues/1934 .. _#1944: https://github.com/pytest-dev/pytest/issues/1944 From afc1e2b0e1cc67c33fcadbb9f83bed56c17d184a Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 18 Oct 2016 17:21:40 +0200 Subject: [PATCH 11/14] docs: remove mention of string args to main fixes #2008 string args got deprecated due to the insane amount of edge-cases wrt splitting on windows vs posix --- doc/en/usage.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/en/usage.rst b/doc/en/usage.rst index f87e1496d..ef63a8e06 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -310,10 +310,6 @@ You can pass in options and arguments:: pytest.main(['-x', 'mytestdir']) -or pass in a string:: - - pytest.main("-x mytestdir") - You can specify additional plugins to ``pytest.main``:: # content of myinvoke.py From 918af99a2ae21c133a0579a8c71e64efdcceba9c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 20 Oct 2016 12:30:58 -0200 Subject: [PATCH 12/14] Remove example of obsolete pytest.main usage with string --- doc/en/goodpractices.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/en/goodpractices.rst b/doc/en/goodpractices.rst index 7c2fdccf2..2a5d4d7c8 100644 --- a/doc/en/goodpractices.rst +++ b/doc/en/goodpractices.rst @@ -236,9 +236,10 @@ your own setuptools Test command for invoking pytest. self.pytest_args = [] def run_tests(self): + import shlex #import here, cause outside the eggs aren't loaded import pytest - errno = pytest.main(self.pytest_args) + errno = pytest.main(shlex.split(self.pytest_args)) sys.exit(errno) From 95007ddeca58bdae6f56b0d04a828463a10bc8e7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 20 Oct 2016 18:56:54 -0200 Subject: [PATCH 13/14] Disable py35-trial while #1989 is not fixed --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2f282e94b..8c36a298c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,8 @@ env: - TESTENV=py27-trial - TESTENV=py35-pexpect - TESTENV=py35-xdist - - TESTENV=py35-trial + # Disable py35-trial temporarily: #1989 + #- TESTENV=py35-trial - TESTENV=py27-nobyte - TESTENV=doctesting - TESTENV=freeze From 620b384b69ffaf0e98af015a721f385ceb13093c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 20 Oct 2016 20:26:14 -0200 Subject: [PATCH 14/14] Fix cmdline help message for custom options with two or more metavars Fix #2004 --- CHANGELOG.rst | 5 +++++ _pytest/config.py | 2 +- testing/test_parseopt.py | 14 +++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 237ea478f..2fb15aa11 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,9 @@ * Import errors when collecting test modules now display the full traceback (`#1976`_). Thanks `@cwitty`_ for the report and `@nicoddemus`_ for the PR. +* Fix confusing command-line help message for custom options with two or more `metavar` properties (`#2004`_). + Thanks `@okulynyak`_ and `@davehunt`_ for the report and `@nicoddemus`_ for the PR. + * When loading plugins, import errors which contain non-ascii messages are now properly handled in Python 2 (`#1998`_). Thanks `@nicoddemus`_ for the PR. @@ -13,9 +16,11 @@ .. _@cwitty: https://github.com/cwitty +.. _@okulynyak: https://github.com/okulynyak .. _#1976: https://github.com/pytest-dev/pytest/issues/1976 .. _#1998: https://github.com/pytest-dev/pytest/issues/1998 +.. _#2004: https://github.com/pytest-dev/pytest/issues/2004 diff --git a/_pytest/config.py b/_pytest/config.py index a169a68a2..5df198e21 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -793,7 +793,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): if len(option) == 2 or option[2] == ' ': return_list.append(option) if option[2:] == short_long.get(option.replace('-', '')): - return_list.append(option.replace(' ', '=')) + return_list.append(option.replace(' ', '=', 1)) action._formatted_action_invocation = ', '.join(return_list) return action._formatted_action_invocation diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index cc9aa23cd..fc9ded488 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -248,7 +248,19 @@ class TestParser: help="show help message and configuration info") parser.parse(['-h']) help = parser.optparser.format_help() - assert '-doit, --func-args foo' in help + assert '-doit, --func-args foo' in help + + def test_multiple_metavar_help(self, parser): + """ + Help text for options with a metavar tuple should display help + in the form "--preferences=value1 value2 value3" (#2004). + """ + group = parser.getgroup("general") + group.addoption('--preferences', metavar=('value1', 'value2', 'value3'), nargs=3) + group._addoption("-h", "--help", action="store_true", dest="help") + parser.parse(['-h']) + help = parser.optparser.format_help() + assert '--preferences=value1 value2 value3' in help def test_argcomplete(testdir, monkeypatch):