diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 34fee3328..87af5d72a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,9 +25,9 @@ Bug Fixes - ``UnicodeWarning`` is issued from the internal pytest warnings plugin only when the message contains non-ascii unicode (Python 2 only). (#2463) -- Added a workaround for Python 3.6 WindowsConsoleIO breaking due to Pytests's - FDCapture. Other code using console handles might still be affected by the - very same issue and might require further workarounds/fixes, i.e. colorama. +- Added a workaround for Python 3.6 ``WindowsConsoleIO`` breaking due to Pytests's + ``FDCapture``. Other code using console handles might still be affected by the + very same issue and might require further workarounds/fixes, i.e. ``colorama``. (#2467) diff --git a/LICENSE b/LICENSE index 9e27bd784..629df45ac 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2004-2016 Holger Krekel and others +Copyright (c) 2004-2017 Holger Krekel and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index abf57fece..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,39 +0,0 @@ -include CHANGELOG.rst -include LICENSE -include AUTHORS -include pyproject.toml - -include README.rst -include CONTRIBUTING.rst -include HOWTORELEASE.rst - -include tox.ini -include setup.py - -recursive-include changelog * -recursive-include scripts *.py -recursive-include scripts *.bat - -include .coveragerc - -recursive-include bench *.py -recursive-include extra *.py - -graft testing -graft doc -prune doc/en/_build -graft tasks - -exclude _pytest/impl - -graft _pytest/vendored_packages - -recursive-exclude * *.pyc *.pyo -recursive-exclude testing/.hypothesis * -recursive-exclude testing/freeze/~ * -recursive-exclude testing/freeze/build * -recursive-exclude testing/freeze/dist * - -exclude appveyor.yml -exclude .travis.yml -prune .github diff --git a/README.rst b/README.rst index 3a0abc5c1..cf2304ed8 100644 --- a/README.rst +++ b/README.rst @@ -102,7 +102,7 @@ Consult the `Changelog `__ page License ------- -Copyright Holger Krekel and others, 2004-2016. +Copyright Holger Krekel and others, 2004-2017. Distributed under the terms of the `MIT`_ license, pytest is free and open source software. diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index 9b3408dc4..5b7cc4191 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -640,8 +640,11 @@ class FormattedExcinfo(object): ).format(exc_type=type(e).__name__, exc_msg=safe_str(e), max_frames=max_frames, total=len(traceback)) traceback = traceback[:max_frames] + traceback[-max_frames:] else: - extraline = "!!! Recursion detected (same locals & position)" - traceback = traceback[:recursionindex + 1] + if recursionindex is not None: + extraline = "!!! Recursion detected (same locals & position)" + traceback = traceback[:recursionindex + 1] + else: + extraline = None return traceback, extraline diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 79943cc53..6ec54d7e7 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -162,10 +162,6 @@ class AssertionRewritingHook(object): # modules not passed explicitly on the command line are only # rewritten if they match the naming convention for test files for pat in self.fnpats: - # use fnmatch instead of fn_pypath.fnmatch because the - # latter might trigger an import to fnmatch.fnmatch - # internally, which would cause this method to be - # called recursively if fn_pypath.fnmatch(pat): state.trace("matched test file %r" % (fn,)) return True diff --git a/_pytest/doctest.py b/_pytest/doctest.py index 46b49d212..fde6dd71d 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -177,7 +177,6 @@ class DoctestTextfile(pytest.Module): name = self.fspath.basename globs = {'__name__': '__main__'} - optionflags = get_optionflags(self) runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, checker=_get_checker()) @@ -218,9 +217,6 @@ class DoctestModule(pytest.Module): runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, checker=_get_checker()) - encoding = self.config.getini("doctest_encoding") - _fix_spoof_python2(runner, encoding) - for test in finder.find(module, module.__name__): if test.examples: # skip empty doctests yield DoctestItem(test.name, self, runner, test) @@ -332,7 +328,10 @@ def _get_report_choice(key): def _fix_spoof_python2(runner, encoding): """ - Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. + Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. This + should patch only doctests for text files because they don't have a way to declare their + encoding. Doctests in docstrings from Python modules don't have the same problem given that + Python already decoded the strings. This fixes the problem related in issue #2434. """ diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 1a6e245c7..64d21b9f6 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -733,10 +733,19 @@ class FixtureDef: self._finalizer.append(finalizer) def finish(self): + exceptions = [] try: while self._finalizer: - func = self._finalizer.pop() - func() + try: + func = self._finalizer.pop() + func() + except: + exceptions.append(sys.exc_info()) + if exceptions: + e = exceptions[0] + del exceptions # ensure we don't keep all frames alive because of the traceback + py.builtin._reraise(*e) + finally: ihook = self._fixturemanager.session.ihook ihook.pytest_fixture_post_finalizer(fixturedef=self) diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index e96fe32cd..2c9a66163 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -73,7 +73,9 @@ def pytest_configure(config): @hookspec(firstresult=True) def pytest_cmdline_parse(pluginmanager, args): - """return initialized config object, parsing the specified args. """ + """return initialized config object, parsing the specified args. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_cmdline_preparse(config, args): """(deprecated) modify command line arguments before option parsing. """ @@ -81,7 +83,9 @@ def pytest_cmdline_preparse(config, args): @hookspec(firstresult=True) def pytest_cmdline_main(config): """ called for performing the main command line action. The default - implementation will invoke the configure hooks and runtest_mainloop. """ + implementation will invoke the configure hooks and runtest_mainloop. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_load_initial_conftests(early_config, parser, args): """ implements the loading of initial conftest files ahead @@ -94,7 +98,9 @@ def pytest_load_initial_conftests(early_config, parser, args): @hookspec(firstresult=True) def pytest_collection(session): - """ perform the collection protocol for the given session. """ + """ perform the collection protocol for the given session. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_collection_modifyitems(session, config, items): """ called after collection has been performed, may filter or re-order @@ -108,11 +114,15 @@ def pytest_ignore_collect(path, config): """ return True to prevent considering this path for collection. This hook is consulted for all files and directories prior to calling more specific hooks. + + Stops at first non-None result, see :ref:`firstresult` """ @hookspec(firstresult=True) def pytest_collect_directory(path, parent): - """ called before traversing a directory for collection files. """ + """ called before traversing a directory for collection files. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_collect_file(path, parent): """ return collection Node or None for the given path. Any new node @@ -133,7 +143,9 @@ def pytest_deselected(items): @hookspec(firstresult=True) def pytest_make_collect_report(collector): - """ perform ``collector.collect()`` and return a CollectReport. """ + """ perform ``collector.collect()`` and return a CollectReport. + + Stops at first non-None result, see :ref:`firstresult` """ # ------------------------------------------------------------------------- # Python test function related hooks @@ -145,15 +157,20 @@ def pytest_pycollect_makemodule(path, parent): This hook will be called for each matching test module path. The pytest_collect_file hook needs to be used if you want to create test modules for files that do not match as a test module. - """ + + Stops at first non-None result, see :ref:`firstresult` """ @hookspec(firstresult=True) def pytest_pycollect_makeitem(collector, name, obj): - """ return custom item/collector for a python object in a module, or None. """ + """ return custom item/collector for a python object in a module, or None. + + Stops at first non-None result, see :ref:`firstresult` """ @hookspec(firstresult=True) def pytest_pyfunc_call(pyfuncitem): - """ call underlying test function. """ + """ call underlying test function. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_generate_tests(metafunc): """ generate (multiple) parametrized calls to a test function.""" @@ -163,7 +180,8 @@ def pytest_make_parametrize_id(config, val, argname): """Return a user-friendly string representation of the given ``val`` that will be used by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. The parameter name is available as ``argname``, if required. - """ + + Stops at first non-None result, see :ref:`firstresult` """ # ------------------------------------------------------------------------- # generic runtest related hooks @@ -172,7 +190,9 @@ def pytest_make_parametrize_id(config, val, argname): @hookspec(firstresult=True) def pytest_runtestloop(session): """ called for performing the main runtest loop - (after collection finished). """ + (after collection finished). + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_itemstart(item, node): """ (deprecated, use pytest_runtest_logstart). """ @@ -190,7 +210,9 @@ def pytest_runtest_protocol(item, nextitem): :py:func:`pytest_runtest_teardown`. :return boolean: True if no further hook implementations should be invoked. - """ + + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_runtest_logstart(nodeid, location): """ signal the start of running a single test item. """ @@ -215,7 +237,8 @@ def pytest_runtest_makereport(item, call): """ return a :py:class:`_pytest.runner.TestReport` object for the given :py:class:`pytest.Item <_pytest.main.Item>` and :py:class:`_pytest.runner.CallInfo`. - """ + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_runtest_logreport(report): """ process a test setup/call/teardown report relating to @@ -227,7 +250,9 @@ def pytest_runtest_logreport(report): @hookspec(firstresult=True) def pytest_fixture_setup(fixturedef, request): - """ performs fixture setup execution. """ + """ performs fixture setup execution. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_fixture_post_finalizer(fixturedef): """ called after fixture teardown, but before the cache is cleared so @@ -277,7 +302,9 @@ def pytest_report_header(config, startdir): @hookspec(firstresult=True) def pytest_report_teststatus(report): - """ return result-category, shortletter and verbose word for reporting.""" + """ return result-category, shortletter and verbose word for reporting. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_terminal_summary(terminalreporter, exitstatus): """ add additional section in terminal summary reporting. """ @@ -295,7 +322,9 @@ def pytest_logwarning(message, code, nodeid, fslocation): @hookspec(firstresult=True) def pytest_doctest_prepare_content(content): - """ return processed content for a given doctest""" + """ return processed content for a given doctest + + Stops at first non-None result, see :ref:`firstresult` """ # ------------------------------------------------------------------------- # error handling and internal debugging hooks diff --git a/_pytest/main.py b/_pytest/main.py index 480810cc8..ec4ec2cc7 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -763,7 +763,11 @@ class Session(FSCollector): if not has_matched and len(rep.result) == 1 and x.name == "()": nextnames.insert(0, name) resultnodes.extend(self.matchnodes([x], nextnames)) - node.ihook.pytest_collectreport(report=rep) + else: + # report collection failures here to avoid failing to run some test + # specified in the command line because the module could not be + # imported (#134) + node.ihook.pytest_collectreport(report=rep) return resultnodes def genitems(self, node): diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 7ad6fef89..9cc404a49 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -27,10 +27,8 @@ def recwarn(): def deprecated_call(func=None, *args, **kwargs): - """ assert that calling ``func(*args, **kwargs)`` triggers a - ``DeprecationWarning`` or ``PendingDeprecationWarning``. - - This function can be used as a context manager:: + """context manager that can be used to ensure a block of code triggers a + ``DeprecationWarning`` or ``PendingDeprecationWarning``:: >>> import warnings >>> def api_call_v2(): @@ -40,38 +38,47 @@ def deprecated_call(func=None, *args, **kwargs): >>> with deprecated_call(): ... assert api_call_v2() == 200 - Note: we cannot use WarningsRecorder here because it is still subject - to the mechanism that prevents warnings of the same type from being - triggered twice for the same module. See #1190. + ``deprecated_call`` can also be used by passing a function and ``*args`` and ``*kwargs``, + in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings + types above. """ if not func: - return WarningsChecker(expected_warning=(DeprecationWarning, PendingDeprecationWarning)) - - categories = [] - - def warn_explicit(message, category, *args, **kwargs): - categories.append(category) - - def warn(message, category=None, *args, **kwargs): - if isinstance(message, Warning): - categories.append(message.__class__) - else: - categories.append(category) - - old_warn = warnings.warn - old_warn_explicit = warnings.warn_explicit - warnings.warn_explicit = warn_explicit - warnings.warn = warn - try: - ret = func(*args, **kwargs) - finally: - warnings.warn_explicit = old_warn_explicit - warnings.warn = old_warn - deprecation_categories = (DeprecationWarning, PendingDeprecationWarning) - if not any(issubclass(c, deprecation_categories) for c in categories): + return _DeprecatedCallContext() + else: __tracebackhide__ = True - raise AssertionError("%r did not produce DeprecationWarning" % (func,)) - return ret + with _DeprecatedCallContext(): + return func(*args, **kwargs) + + +class _DeprecatedCallContext(object): + """Implements the logic to capture deprecation warnings as a context manager.""" + + def __enter__(self): + self._captured_categories = [] + self._old_warn = warnings.warn + self._old_warn_explicit = warnings.warn_explicit + warnings.warn_explicit = self._warn_explicit + warnings.warn = self._warn + + def _warn_explicit(self, message, category, *args, **kwargs): + self._captured_categories.append(category) + + def _warn(self, message, category=None, *args, **kwargs): + if isinstance(message, Warning): + self._captured_categories.append(message.__class__) + else: + self._captured_categories.append(category) + + def __exit__(self, exc_type, exc_val, exc_tb): + warnings.warn_explicit = self._old_warn_explicit + warnings.warn = self._old_warn + + if exc_type is None: + deprecation_categories = (DeprecationWarning, PendingDeprecationWarning) + if not any(issubclass(c, deprecation_categories) for c in self._captured_categories): + __tracebackhide__ = True + msg = "Did not produce DeprecationWarning or PendingDeprecationWarning" + raise AssertionError(msg) def warns(expected_warning, *args, **kwargs): diff --git a/_pytest/terminal.py b/_pytest/terminal.py index e226d607b..af89d0fc2 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -282,7 +282,7 @@ class TerminalReporter: line = "collected " else: line = "collecting " - line += str(self._numcollected) + " items" + line += str(self._numcollected) + " item" + ('' if self._numcollected == 1 else 's') if errors: line += " / %d errors" % errors if skipped: diff --git a/changelog/2434.bugfix b/changelog/2434.bugfix new file mode 100644 index 000000000..172a992c4 --- /dev/null +++ b/changelog/2434.bugfix @@ -0,0 +1 @@ +Fix decode error in Python 2 for doctests in docstrings. diff --git a/changelog/2440.bugfix b/changelog/2440.bugfix new file mode 100644 index 000000000..7f1f7d504 --- /dev/null +++ b/changelog/2440.bugfix @@ -0,0 +1 @@ +Exceptions raised during teardown by finalizers are now suppressed until all finalizers are called, with the initial exception reraised. diff --git a/changelog/2464.bugfix b/changelog/2464.bugfix new file mode 100644 index 000000000..12062fd9e --- /dev/null +++ b/changelog/2464.bugfix @@ -0,0 +1 @@ +Fix incorrect "collected items" report when specifying tests on the command-line. diff --git a/changelog/2469.bugfix b/changelog/2469.bugfix new file mode 100644 index 000000000..492c62e08 --- /dev/null +++ b/changelog/2469.bugfix @@ -0,0 +1,4 @@ +``deprecated_call`` in context-manager form now captures deprecation warnings even if +the same warning has already been raised. Also, ``deprecated_call`` will always produce +the same error message (previously it would produce different messages in context-manager vs. +function-call mode). diff --git a/changelog/2474.trivial b/changelog/2474.trivial new file mode 100644 index 000000000..9ea3fb651 --- /dev/null +++ b/changelog/2474.trivial @@ -0,0 +1 @@ +Create invoke tasks for updating the vendored packages. \ No newline at end of file diff --git a/changelog/2486.bugfix b/changelog/2486.bugfix new file mode 100644 index 000000000..97917197c --- /dev/null +++ b/changelog/2486.bugfix @@ -0,0 +1 @@ +Fix internal error when trying to detect the start of a recursive traceback. diff --git a/changelog/2493.doc b/changelog/2493.doc new file mode 100644 index 000000000..619963041 --- /dev/null +++ b/changelog/2493.doc @@ -0,0 +1 @@ +Explicitly state for which hooks the calls stop after the first non-None result. \ No newline at end of file diff --git a/changelog/2499.trivial b/changelog/2499.trivial new file mode 100644 index 000000000..1b4341725 --- /dev/null +++ b/changelog/2499.trivial @@ -0,0 +1 @@ +Update copyright dates in LICENSE, README.rst and in the documentation. diff --git a/changelog/_template.rst b/changelog/_template.rst index 66fd6ae56..66c850ffd 100644 --- a/changelog/_template.rst +++ b/changelog/_template.rst @@ -6,14 +6,14 @@ {% endif %} {% if sections[section] %} -{% for category, val in definitions.items() if category in sections[section] and category != 'trivial' %} +{% for category, val in definitions.items() if category in sections[section] %} {{ definitions[category]['name'] }} {{ underline * definitions[category]['name']|length }} {% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category]|dictsort(by='value') %} -- {{ text }}{% if category != 'vendor' %} ({{ values|sort|join(', ') }}){% endif %} +- {{ text }}{% if category != 'vendor' %} (`{{ values[0] }} `_){% endif %} {% endfor %} diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index f760c423e..c5bf67053 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -73,20 +73,20 @@ marked ``smtp`` fixture function. Running the test looks like this:: platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 items - + test_smtpsimple.py F - + ======= FAILURES ======== _______ test_ehlo ________ - + smtp = - + def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 > assert 0 # for demo purposes E assert 0 - + test_smtpsimple.py:11: AssertionError ======= 1 failed in 0.12 seconds ======== @@ -123,20 +123,13 @@ with a list of available function arguments. but is not anymore advertised as the primary means of declaring fixture functions. -"Funcargs" a prime example of dependency injection +Fixtures: a prime example of dependency injection --------------------------------------------------- -When injecting fixtures to test functions, pytest-2.0 introduced the -term "funcargs" or "funcarg mechanism" which continues to be present -also in docs today. It now refers to the specific case of injecting -fixture values as arguments to test functions. With pytest-2.3 there are -more possibilities to use fixtures but "funcargs" remain as the main way -as they allow to directly state the dependencies of a test function. - -As the following examples show in more detail, funcargs allow test -functions to easily receive and work against specific pre-initialized -application objects without having to care about import/setup/cleanup -details. It's a prime example of `dependency injection`_ where fixture +Fixtures allow test functions to easily receive and work +against specific pre-initialized application objects without having +to care about import/setup/cleanup details. +It's a prime example of `dependency injection`_ where fixture functions take the role of the *injector* and test functions are the *consumers* of fixture objects. @@ -176,7 +169,7 @@ function (in or below the directory where ``conftest.py`` is located):: response, msg = smtp.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg - assert 0 # for demo purposes + assert 0 # for demo purposes def test_noop(smtp): response, msg = smtp.noop() @@ -191,32 +184,32 @@ inspect what is going on and can now run the tests:: platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items - + test_module.py FF - + ======= FAILURES ======== _______ test_ehlo ________ - + smtp = - + def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg > assert 0 # for demo purposes E assert 0 - + test_module.py:6: AssertionError _______ test_noop ________ - + smtp = - + def test_noop(smtp): response, msg = smtp.noop() assert response == 250 > assert 0 # for demo purposes E assert 0 - + test_module.py:11: AssertionError ======= 2 failed in 0.12 seconds ======== @@ -267,7 +260,7 @@ Let's execute it:: $ pytest -s -q --tb=no FFteardown smtp - + 2 failed in 0.12 seconds We see that the ``smtp`` instance is finalized after the two @@ -296,6 +289,9 @@ The ``smtp`` connection will be closed after the test finished execution because the ``smtp`` object automatically closes when the ``with`` statement ends. +Note that if an exception happens during the *setup* code (before the ``yield`` keyword), the +*teardown* code (after the ``yield``) will not be called. + .. note:: Prior to version 2.10, in order to use a ``yield`` statement to execute teardown code one @@ -303,29 +299,51 @@ the ``with`` statement ends. fixtures can use ``yield`` directly so the ``yield_fixture`` decorator is no longer needed and considered deprecated. -.. note:: - As historical note, another way to write teardown code is - by accepting a ``request`` object into your fixture function and can call its - ``request.addfinalizer`` one or multiple times:: - # content of conftest.py +An alternative option for executing *teardown* code is to +make use of the ``addfinalizer`` method of the `request-context`_ object to register +finalization functions. - import smtplib - import pytest +Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup: - @pytest.fixture(scope="module") - def smtp(request): - smtp = smtplib.SMTP("smtp.gmail.com") - def fin(): - print ("teardown smtp") - smtp.close() - request.addfinalizer(fin) - return smtp # provide the fixture value +.. code-block:: python - The ``fin`` function will execute when the last test in the module has finished execution. + # content of conftest.py + import smtplib + import pytest + + @pytest.fixture(scope="module") + def smtp(request): + smtp = smtplib.SMTP("smtp.gmail.com") + def fin(): + print ("teardown smtp") + smtp.close() + request.addfinalizer(fin) + return smtp # provide the fixture value + + +Both ``yield`` and ``addfinalizer`` methods work similarly by calling their code after the test +ends, but ``addfinalizer`` has two key differences over ``yield``: + +1. It is possible to register multiple finalizer functions. + +2. Finalizers will always be called regardless if the fixture *setup* code raises an exception. + This is handy to properly close all resources created by a fixture even if one of them + fails to be created/acquired:: + + @pytest.fixture + def equipments(request): + r = [] + for port in ('C1', 'C3', 'C28'): + equip = connect(port) + request.addfinalizer(equip.disconnect) + r.append(equip) + return r + + In the example above, if ``"C28"`` fails with an exception, ``"C1"`` and ``"C3"`` will still + be properly closed. Of course, if an exception happens before the finalize function is + registered then it will not be executed. - This method is still fully supported, but ``yield`` is recommended from 2.10 onward because - it is considered simpler and better describes the natural code flow. .. _`request-context`: @@ -355,7 +373,7 @@ again, nothing much has changed:: $ pytest -s -q --tb=no FFfinalizing (smtp.gmail.com) - + 2 failed in 0.12 seconds Let's quickly create another test module that actually sets the @@ -423,51 +441,51 @@ So let's just do another run:: FFFF ======= FAILURES ======== _______ test_ehlo[smtp.gmail.com] ________ - + smtp = - + def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg > assert 0 # for demo purposes E assert 0 - + test_module.py:6: AssertionError _______ test_noop[smtp.gmail.com] ________ - + smtp = - + def test_noop(smtp): response, msg = smtp.noop() assert response == 250 > assert 0 # for demo purposes E assert 0 - + test_module.py:11: AssertionError _______ test_ehlo[mail.python.org] ________ - + smtp = - + def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 > assert b"smtp.gmail.com" in msg E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nSIZE 51200000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8' - + test_module.py:5: AssertionError -------------------------- Captured stdout setup --------------------------- finalizing _______ test_noop[mail.python.org] ________ - + smtp = - + def test_noop(smtp): response, msg = smtp.noop() assert response == 250 > assert 0 # for demo purposes E assert 0 - + test_module.py:11: AssertionError ------------------------- Captured stdout teardown ------------------------- finalizing @@ -539,7 +557,7 @@ Running the above tests results in the following test IDs being used:: - + ======= no tests ran in 0.12 seconds ======== .. _`interdependent fixtures`: @@ -578,10 +596,10 @@ Here we declare an ``app`` fixture which receives the previously defined cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items - + test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED test_appsetup.py::test_smtp_exists[mail.python.org] PASSED - + ======= 2 passed in 0.12 seconds ======== Due to the parametrization of ``smtp`` the test will run twice with two @@ -647,26 +665,26 @@ Let's run the tests in verbose mode and with looking at the print-output:: cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 8 items - + test_module.py::test_0[1] SETUP otherarg 1 RUN test0 with otherarg 1 PASSED TEARDOWN otherarg 1 - + test_module.py::test_0[2] SETUP otherarg 2 RUN test0 with otherarg 2 PASSED TEARDOWN otherarg 2 - + test_module.py::test_1[mod1] SETUP modarg mod1 RUN test1 with modarg mod1 PASSED test_module.py::test_2[1-mod1] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod1 PASSED TEARDOWN otherarg 1 - + test_module.py::test_2[2-mod1] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod1 PASSED TEARDOWN otherarg 2 - + test_module.py::test_1[mod2] TEARDOWN modarg mod1 SETUP modarg mod2 RUN test1 with modarg mod2 @@ -674,13 +692,13 @@ Let's run the tests in verbose mode and with looking at the print-output:: test_module.py::test_2[1-mod2] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod2 PASSED TEARDOWN otherarg 1 - + test_module.py::test_2[2-mod2] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod2 PASSED TEARDOWN otherarg 2 TEARDOWN modarg mod2 - - + + ======= 8 passed in 0.12 seconds ======== You can see that the parametrized module-scoped ``modarg`` resource caused an @@ -782,8 +800,8 @@ Autouse fixtures (xUnit setup on steroids) .. regendoc:wipe Occasionally, you may want to have fixtures get invoked automatically -without a `usefixtures`_ or `funcargs`_ reference. As a practical -example, suppose we have a database fixture which has a +without declaring a function argument explicitly or a `usefixtures`_ decorator. +As a practical example, suppose we have a database fixture which has a begin/rollback/commit architecture and we want to automatically surround each test method by a transaction and a rollback. Here is a dummy self-contained implementation of this idea:: diff --git a/doc/en/index.rst b/doc/en/index.rst index cb901b8d5..77e019d70 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -83,8 +83,8 @@ Consult the :ref:`Changelog ` page for fixes and enhancements of each License ------- -Copyright Holger Krekel and others, 2004-2016. +Copyright Holger Krekel and others, 2004-2017. Distributed under the terms of the `MIT`_ license, pytest is free and open source software. -.. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE \ No newline at end of file +.. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE diff --git a/doc/en/license.rst b/doc/en/license.rst index 3fc1dad52..b8c0dce1b 100644 --- a/doc/en/license.rst +++ b/doc/en/license.rst @@ -9,7 +9,7 @@ Distributed under the terms of the `MIT`_ license, pytest is free and open sourc The MIT License (MIT) - Copyright (c) 2004-2016 Holger Krekel and others + Copyright (c) 2004-2017 Holger Krekel and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 38bbe887d..9f5190c3e 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -255,11 +255,11 @@ if ``myapp.testsupport.myplugin`` also declares ``pytest_plugins``, the contents of the variable will also be loaded as plugins, and so on. This mechanism makes it easy to share fixtures within applications or even -external applications without the need to create external plugins using +external applications without the need to create external plugins using the ``setuptools``'s entry point technique. Plugins imported by ``pytest_plugins`` will also automatically be marked -for assertion rewriting (see :func:`pytest.register_assert_rewrite`). +for assertion rewriting (see :func:`pytest.register_assert_rewrite`). However for this to have any effect the module must not be imported already; if it was already imported at the time the ``pytest_plugins`` statement is processed, a warning will result and @@ -357,6 +357,8 @@ allowed to raise exceptions. Doing so will break the pytest run. +.. _firstresult: + firstresult: stop at first non-None result ------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 138fd4ce6..88571e208 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,5 +31,5 @@ template = "changelog/_template.rst" [[tool.towncrier.type]] directory = "trivial" - name = "Trivial Changes" - showcontent = false + name = "Trivial/Internal Changes" + showcontent = true diff --git a/scripts/check-manifest.py b/scripts/check-manifest.py deleted file mode 100644 index de1357685..000000000 --- a/scripts/check-manifest.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Script used by tox.ini to check the manifest file if we are under version control, or skip the -check altogether if not. - -"check-manifest" will needs a vcs to work, which is not available when testing the package -instead of the source code (with ``devpi test`` for example). -""" - -from __future__ import print_function - -import os -import subprocess -import sys -from check_manifest import main - -if os.path.isdir('.git'): - sys.exit(main()) -else: - print('No .git directory found, skipping checking the manifest file') - sys.exit(0) diff --git a/tasks/__init__.py b/tasks/__init__.py index 9551ff059..992f4a4ad 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -4,6 +4,10 @@ Invoke tasks to help with pytest development and release process. import invoke -from . import generate +from . import generate, vendoring -ns = invoke.Collection(generate) + +ns = invoke.Collection( + generate, + vendoring +) diff --git a/tasks/vendoring.py b/tasks/vendoring.py new file mode 100644 index 000000000..867f2946b --- /dev/null +++ b/tasks/vendoring.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, print_function +import py +import invoke + +VENDOR_TARGET = py.path.local("_pytest/vendored_packages") +GOOD_FILES = 'README.md', '__init__.py' + +@invoke.task() +def remove_libs(ctx): + print("removing vendored libs") + for path in VENDOR_TARGET.listdir(): + if path.basename not in GOOD_FILES: + print(" ", path) + path.remove() + +@invoke.task(pre=[remove_libs]) +def update_libs(ctx): + print("installing libs") + ctx.run("pip install -t {target} pluggy".format(target=VENDOR_TARGET)) + ctx.run("git add {target}".format(target=VENDOR_TARGET)) + print("Please commit to finish the update after running the tests:") + print() + print(' git commit -am "Updated vendored libs"') diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 00abfc38d..9cb2650de 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -317,8 +317,8 @@ class TestGeneralUsage(object): ]) assert 'sessionstarttime' not in result.stderr.str() - @pytest.mark.parametrize('lookfor', ['test_fun.py', 'test_fun.py::test_a']) - def test_issue134_report_syntaxerror_when_collecting_member(self, testdir, lookfor): + @pytest.mark.parametrize('lookfor', ['test_fun.py::test_a']) + def test_issue134_report_error_when_collecting_member(self, testdir, lookfor): testdir.makepyfile(test_fun=""" def test_a(): pass diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 3128beff8..0b074d64a 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function +import sys import operator import _pytest import py @@ -1173,3 +1174,25 @@ def test_exception_repr_extraction_error_on_recursion(): '*The following exception happened*', '*ValueError: The truth value of an array*', ]) + + +def test_no_recursion_index_on_recursion_error(): + """ + Ensure that we don't break in case we can't find the recursion index + during a recursion error (#2486). + """ + try: + class RecursionDepthError(object): + def __getattr__(self, attr): + return getattr(self, '_' + attr) + + RecursionDepthError().trigger + except: + from _pytest._code.code import ExceptionInfo + exc_info = ExceptionInfo() + if sys.version_info[:2] == (2, 6): + assert "'RecursionDepthError' object has no attribute '___" in str(exc_info.getrepr()) + else: + assert 'maximum recursion' in str(exc_info.getrepr()) + else: + assert 0 diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 4c9ad7a91..1e58a5550 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -657,6 +657,39 @@ class TestRequestBasic(object): "*1 error*" # XXX the whole module collection fails ]) + def test_request_subrequest_addfinalizer_exceptions(self, testdir): + """ + Ensure exceptions raised during teardown by a finalizer are suppressed + until all finalizers are called, re-raising the first exception (#2440) + """ + testdir.makepyfile(""" + import pytest + l = [] + def _excepts(where): + raise Exception('Error in %s fixture' % where) + @pytest.fixture + def subrequest(request): + return request + @pytest.fixture + def something(subrequest): + subrequest.addfinalizer(lambda: l.append(1)) + subrequest.addfinalizer(lambda: l.append(2)) + subrequest.addfinalizer(lambda: _excepts('something')) + @pytest.fixture + def excepts(subrequest): + subrequest.addfinalizer(lambda: _excepts('excepts')) + subrequest.addfinalizer(lambda: l.append(3)) + def test_first(something, excepts): + pass + def test_second(): + assert l == [3, 2, 1] + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*Exception: Error in excepts fixture', + '* 2 passed, 1 error in *', + ]) + def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") item, = testdir.genitems([modcol]) diff --git a/testing/test_collection.py b/testing/test_collection.py index c19fc0e72..a90269789 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -369,6 +369,11 @@ class TestSession(object): assert len(colitems) == 1 assert colitems[0].fspath == p + def get_reported_items(self, hookrec): + """Return pytest.Item instances reported by the pytest_collectreport hook""" + calls = hookrec.getcalls('pytest_collectreport') + return [x for call in calls for x in call.report.result + if isinstance(x, pytest.Item)] def test_collect_protocol_single_function(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -386,9 +391,10 @@ class TestSession(object): ("pytest_collectstart", "collector.fspath == p"), ("pytest_make_collect_report", "collector.fspath == p"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), - ("pytest_collectreport", "report.nodeid == ''") + ("pytest_collectreport", "report.result[0].name == 'test_func'"), ]) + # ensure we are reporting the collection of the single test item (#2464) + assert [x.name for x in self.get_reported_items(hookrec)] == ['test_func'] def test_collect_protocol_method(self, testdir): p = testdir.makepyfile(""" @@ -407,6 +413,8 @@ class TestSession(object): assert items[0].name == "test_method" newid = items[0].nodeid assert newid == normid + # ensure we are reporting the collection of the single test item (#2464) + assert [x.name for x in self.get_reported_items(hookrec)] == ['test_method'] def test_collect_custom_nodes_multi_id(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -436,9 +444,8 @@ class TestSession(object): "collector.__class__.__name__ == 'Module'"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), - #("pytest_collectreport", - # "report.fspath == %r" % str(rcol.fspath)), ]) + assert len(self.get_reported_items(hookrec)) == 2 def test_collect_subdir_event_ordering(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -495,11 +502,13 @@ class TestSession(object): def test_method(self): pass """) - arg = p.basename + ("::TestClass::test_method") + arg = p.basename + "::TestClass::test_method" items, hookrec = testdir.inline_genitems(arg) assert len(items) == 1 item, = items assert item.nodeid.endswith("TestClass::()::test_method") + # ensure we are reporting the collection of the single test item (#2464) + assert [x.name for x in self.get_reported_items(hookrec)] == ['test_method'] class Test_getinitialnodes(object): def test_global_file(self, testdir, tmpdir): diff --git a/testing/test_doctest.py b/testing/test_doctest.py index e22976c75..26f9c8469 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -527,6 +527,25 @@ class TestDoctests(object): '*1 failed*', ]) + def test_unicode_doctest_module(self, testdir): + """ + Test case for issue 2434: DecodeError on Python 2 when doctest docstring + contains non-ascii characters. + """ + p = testdir.makepyfile(test_unicode_doctest_module=""" + # -*- encoding: utf-8 -*- + from __future__ import unicode_literals + + def fix_bad_unicode(text): + ''' + >>> print(fix_bad_unicode('único')) + único + ''' + return "único" + """) + result = testdir.runpytest(p, '--doctest-modules') + result.stdout.fnmatch_lines(['* 1 passed *']) + class TestLiterals(object): diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 75dacc040..f1048f07d 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -77,7 +77,7 @@ class TestDeprecatedCall(object): def test_deprecated_call_raises(self): with pytest.raises(AssertionError) as excinfo: pytest.deprecated_call(self.dep, 3, 5) - assert str(excinfo).find("did not produce") != -1 + assert 'Did not produce' in str(excinfo) def test_deprecated_call(self): pytest.deprecated_call(self.dep, 0, 5) @@ -106,31 +106,69 @@ class TestDeprecatedCall(object): pytest.deprecated_call(self.dep_explicit, 0) pytest.deprecated_call(self.dep_explicit, 0) - def test_deprecated_call_as_context_manager_no_warning(self): - with pytest.raises(pytest.fail.Exception, matches='^DID NOT WARN'): - with pytest.deprecated_call(): - self.dep(1) + @pytest.mark.parametrize('mode', ['context_manager', 'call']) + def test_deprecated_call_no_warning(self, mode): + """Ensure deprecated_call() raises the expected failure when its block/function does + not raise a deprecation warning. + """ + def f(): + pass + + msg = 'Did not produce DeprecationWarning or PendingDeprecationWarning' + with pytest.raises(AssertionError, matches=msg): + if mode == 'call': + pytest.deprecated_call(f) + else: + with pytest.deprecated_call(): + f() @pytest.mark.parametrize('warning_type', [PendingDeprecationWarning, DeprecationWarning]) @pytest.mark.parametrize('mode', ['context_manager', 'call']) - def test_deprecated_call_modes(self, warning_type, mode): + @pytest.mark.parametrize('call_f_first', [True, False]) + def test_deprecated_call_modes(self, warning_type, mode, call_f_first): + """Ensure deprecated_call() captures a deprecation warning as expected inside its + block/function. + """ def f(): warnings.warn(warning_type("hi")) - + return 10 + + # ensure deprecated_call() can capture the warning even if it has already been triggered + if call_f_first: + assert f() == 10 if mode == 'call': - pytest.deprecated_call(f) + assert pytest.deprecated_call(f) == 10 else: with pytest.deprecated_call(): - f() + assert f() == 10 + + @pytest.mark.parametrize('mode', ['context_manager', 'call']) + def test_deprecated_call_exception_is_raised(self, mode): + """If the block of the code being tested by deprecated_call() raises an exception, + it must raise the exception undisturbed. + """ + def f(): + raise ValueError('some exception') + + with pytest.raises(ValueError, match='some exception'): + if mode == 'call': + pytest.deprecated_call(f) + else: + with pytest.deprecated_call(): + f() def test_deprecated_call_specificity(self): other_warnings = [Warning, UserWarning, SyntaxWarning, RuntimeWarning, FutureWarning, ImportWarning, UnicodeWarning] for warning in other_warnings: def f(): - py.std.warnings.warn(warning("hi")) + warnings.warn(warning("hi")) + with pytest.raises(AssertionError): pytest.deprecated_call(f) + with pytest.raises(AssertionError): + with pytest.deprecated_call(): + f() def test_deprecated_function_already_called(self, testdir): """deprecated_call should be able to catch a call to a deprecated diff --git a/testing/test_runner.py b/testing/test_runner.py index 51d430fc8..def80ea5f 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -513,12 +513,12 @@ def test_pytest_no_tests_collected_exit_status(testdir): assert 1 """) result = testdir.runpytest() - result.stdout.fnmatch_lines('*collected 1 items*') + result.stdout.fnmatch_lines('*collected 1 item*') result.stdout.fnmatch_lines('*1 passed*') assert result.ret == main.EXIT_OK result = testdir.runpytest('-k nonmatch') - result.stdout.fnmatch_lines('*collected 1 items*') + result.stdout.fnmatch_lines('*collected 1 item*') result.stdout.fnmatch_lines('*1 deselected*') assert result.ret == main.EXIT_NOTESTSCOLLECTED diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 5a90b3dd4..45c354206 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -204,6 +204,15 @@ class TestTerminal(object): assert result.ret == 2 result.stdout.fnmatch_lines(['*KeyboardInterrupt*']) + def test_collect_single_item(self, testdir): + """Use singular 'item' when reporting a single test item""" + testdir.makepyfile(""" + def test_foobar(): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(['collected 1 item']) + class TestCollectonly(object): def test_collectonly_basic(self, testdir): diff --git a/tox.ini b/tox.ini index b73deca7d..d8b27300f 100644 --- a/tox.ini +++ b/tox.ini @@ -57,9 +57,7 @@ deps = # pygments required by rst-lint pygments restructuredtext_lint - check-manifest commands = - {envpython} scripts/check-manifest.py flake8 pytest.py _pytest testing {envpython} scripts/check-rst.py