diff --git a/.gitignore b/.gitignore index f5cd0145c..e2d59502c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ coverage.xml .pydevproject .project .settings +.vscode diff --git a/AUTHORS b/AUTHORS index baf2b9123..fd98f8141 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,11 +12,13 @@ Alan Velasco Alexander Johnson Alexei Kozlenok Allan Feldman +Aly Sivji Anatoly Bubenkoff Anders Hovmöller Andras Tim Andrea Cimatoribus Andreas Zeidler +Andrey Paramonov Andrzej Ostrowski Andy Freeland Anthon van der Neut @@ -165,6 +167,7 @@ Miro Hrončok Nathaniel Waisbrot Ned Batchelder Neven Mundar +Nicholas Devenish Niclas Olofsson Nicolas Delaby Oleg Pidsadnyi diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f63fd0813..e7784b931 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,232 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.1.0 (2019-01-05) +========================= + +Removals +-------- + +- `#2169 `_: ``pytest.mark.parametrize``: in previous versions, errors raised by id functions were suppressed and changed into warnings. Now the exceptions are propagated, along with a pytest message informing the node, parameter value and index where the exception occurred. + + +- `#3078 `_: Remove legacy internal warnings system: ``config.warn``, ``Node.warn``. The ``pytest_logwarning`` now issues a warning when implemented. + + See our `docs `__ on information on how to update your code. + + +- `#3079 `_: Removed support for yield tests - they are fundamentally broken because they don't support fixtures properly since collection and test execution were separated. + + See our `docs `__ on information on how to update your code. + + +- `#3082 `_: Removed support for applying marks directly to values in ``@pytest.mark.parametrize``. Use ``pytest.param`` instead. + + See our `docs `__ on information on how to update your code. + + +- `#3083 `_: Removed ``Metafunc.addcall``. This was the predecessor mechanism to ``@pytest.mark.parametrize``. + + See our `docs `__ on information on how to update your code. + + +- `#3085 `_: Removed support for passing strings to ``pytest.main``. Now, always pass a list of strings instead. + + See our `docs `__ on information on how to update your code. + + +- `#3086 `_: ``[pytest]`` section in **setup.cfg** files is not longer supported, use ``[tool:pytest]`` instead. ``setup.cfg`` files + are meant for use with ``distutils``, and a section named ``pytest`` has notoriously been a source of conflicts and bugs. + + Note that for **pytest.ini** and **tox.ini** files the section remains ``[pytest]``. + + +- `#3616 `_: Removed the deprecated compat properties for ``node.Class/Function/Module`` - use ``pytest.Class/Function/Module`` now. + + See our `docs `__ on information on how to update your code. + + +- `#4421 `_: Removed the implementation of the ``pytest_namespace`` hook. + + See our `docs `__ on information on how to update your code. + + +- `#4489 `_: Removed ``request.cached_setup``. This was the predecessor mechanism to modern fixtures. + + See our `docs `__ on information on how to update your code. + + +- `#4535 `_: Removed the deprecated ``PyCollector.makeitem`` method. This method was made public by mistake a long time ago. + + +- `#4543 `_: Removed support to define fixtures using the ``pytest_funcarg__`` prefix. Use the ``@pytest.fixture`` decorator instead. + + See our `docs `__ on information on how to update your code. + + +- `#4545 `_: Calling fixtures directly is now always an error instead of a warning. + + See our `docs `__ on information on how to update your code. + + +- `#4546 `_: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check. + + Use ``Node.get_closest_marker(name)`` as a replacement. + + +- `#4547 `_: The deprecated ``record_xml_property`` fixture has been removed, use the more generic ``record_property`` instead. + + See our `docs `__ for more information. + + +- `#4548 `_: An error is now raised if the ``pytest_plugins`` variable is defined in a non-top-level ``conftest.py`` file (i.e., not residing in the ``rootdir``). + + See our `docs `__ for more information. + + +- `#891 `_: Remove ``testfunction.markername`` attributes - use ``Node.iter_markers(name=None)`` to iterate them. + + + +Deprecations +------------ + +- `#3050 `_: Deprecated the ``pytest.config`` global. + + See https://docs.pytest.org/en/latest/deprecations.html#pytest-config-global for rationale. + + +- `#3974 `_: Passing the ``message`` parameter of ``pytest.raises`` now issues a ``DeprecationWarning``. + + It is a common mistake to think this parameter will match the exception message, while in fact + it only serves to provide a custom message in case the ``pytest.raises`` check fails. To avoid this + mistake and because it is believed to be little used, pytest is deprecating it without providing + an alternative for the moment. + + If you have concerns about this, please comment on `issue #3974 `__. + + +- `#4435 `_: Deprecated ``raises(..., 'code(as_a_string)')`` and ``warns(..., 'code(as_a_string)')``. + + See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec for rationale and examples. + + + +Features +-------- + +- `#3191 `_: A warning is now issued when assertions are made for ``None``. + + This is a common source of confusion among new users, which write: + + .. code-block:: python + + assert mocked_object.assert_called_with(3, 4, 5, key="value") + + When they should write: + + .. code-block:: python + + mocked_object.assert_called_with(3, 4, 5, key="value") + + Because the ``assert_called_with`` method of mock objects already executes an assertion. + + This warning will not be issued when ``None`` is explicitly checked. An assertion like: + + .. code-block:: python + + assert variable is None + + will not issue the warning. + + +- `#3632 `_: Richer equality comparison introspection on ``AssertionError`` for objects created using `attrs `__ or `dataclasses `_ (Python 3.7+, `backported to 3.6 `__). + + +- `#4278 `_: ``CACHEDIR.TAG`` files are now created inside cache directories. + + Those files are part of the `Cache Directory Tagging Standard `__, and can + be used by backup or synchronization programs to identify pytest's cache directory as such. + + +- `#4292 `_: ``pytest.outcomes.Exit`` is derived from ``SystemExit`` instead of ``KeyboardInterrupt``. This allows us to better handle ``pdb`` exiting. + + +- `#4371 `_: Updated the ``--collect-only`` option to display test descriptions when ran using ``--verbose``. + + +- `#4386 `_: Restructured ``ExceptionInfo`` object construction and ensure incomplete instances have a ``repr``/``str``. + + +- `#4416 `_: pdb: added support for keyword arguments with ``pdb.set_trace``. + + It handles ``header`` similar to Python 3.7 does it, and forwards any + other keyword arguments to the ``Pdb`` constructor. + + This allows for ``__import__("pdb").set_trace(skip=["foo.*"])``. + + +- `#4483 `_: Added ini parameter ``junit_duration_report`` to optionally report test call durations, excluding setup and teardown times. + + The JUnit XML specification and the default pytest behavior is to include setup and teardown times in the test duration + report. You can include just the call durations instead (excluding setup and teardown) by adding this to your ``pytest.ini`` file: + + .. code-block:: ini + + [pytest] + junit_duration_report = call + + +- `#4532 `_: ``-ra`` now will show errors and failures last, instead of as the first items in the summary. + + This makes it easier to obtain a list of errors and failures to run tests selectively. + + +- `#4599 `_: ``pytest.importorskip`` now supports a ``reason`` parameter, which will be shown when the + requested module cannot be imported. + + + +Bug Fixes +--------- + +- `#3532 `_: ``-p`` now accepts its argument without a space between the value, for example ``-pmyplugin``. + + +- `#4327 `_: ``approx`` again works with more generic containers, more precisely instances of ``Iterable`` and ``Sized`` instead of more restrictive ``Sequence``. + + +- `#4397 `_: Ensure that node ids are printable. + + +- `#4435 `_: Fixed ``raises(..., 'code(string)')`` frame filename. + + +- `#4458 `_: Display actual test ids in ``--collect-only``. + + + +Improved Documentation +---------------------- + +- `#4557 `_: Markers example documentation page updated to support latest pytest version. + + +- `#4558 `_: Update cache documentation example to correctly show cache hit and miss. + + +- `#4580 `_: Improved detailed summary report documentation. + + + +Trivial/Internal Changes +------------------------ + +- `#4447 `_: Changed the deprecation type of ``--result-log`` to ``PytestDeprecationWarning``. + + It was decided to remove this feature at the next major revision. + + pytest 4.0.2 (2018-12-13) ========================= @@ -1757,7 +1983,7 @@ Bug Fixes Trivial/Internal Changes ------------------------ -- pytest now depends on `attrs `_ for internal +- pytest now depends on `attrs `__ for internal structures to ease code maintainability. (`#2641 `_) diff --git a/changelog/4557.doc.rst b/changelog/4557.doc.rst deleted file mode 100644 index dba2e39cd..000000000 --- a/changelog/4557.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Markers example documentation page updated to support latest pytest version. diff --git a/changelog/4558.doc.rst b/changelog/4558.doc.rst deleted file mode 100644 index 09dc5b863..000000000 --- a/changelog/4558.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Update cache documentation example to correctly show cache hit and miss. diff --git a/changelog/4580.doc.rst b/changelog/4580.doc.rst deleted file mode 100644 index 2d8d52f33..000000000 --- a/changelog/4580.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Improved detailed summary report documentation. diff --git a/doc/en/Makefile b/doc/en/Makefile index fa8e8266a..f93d84557 100644 --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -39,7 +39,7 @@ clean: -rm -rf $(BUILDDIR)/* regen: - PYTHONDONTWRITEBYTECODE=1 PYTEST_ADDOPT=-pno:hypothesis COLUMNS=76 regendoc --update *.rst */*.rst ${REGENDOC_ARGS} + PYTHONDONTWRITEBYTECODE=1 PYTEST_ADDOPTS=-pno:hypothesis COLUMNS=76 regendoc --update *.rst */*.rst ${REGENDOC_ARGS} html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index d6379f1b3..40734e5b3 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.1.0 release-4.0.2 release-4.0.1 release-4.0.0 diff --git a/doc/en/announce/release-4.1.0.rst b/doc/en/announce/release-4.1.0.rst new file mode 100644 index 000000000..b7a076f61 --- /dev/null +++ b/doc/en/announce/release-4.1.0.rst @@ -0,0 +1,44 @@ +pytest-4.1.0 +======================================= + +The pytest team is proud to announce the 4.1.0 release! + +pytest is a mature Python testing tool with more than a 2000 tests +against itself, passing on many different interpreters and platforms. + +This release contains a number of bugs fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/latest/ + +As usual, you can upgrade from pypi via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* Adam Johnson +* Aly Sivji +* Andrey Paramonov +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* David Vo +* Hyunchel Kim +* Jeffrey Rackauckas +* Kanguros +* Nicholas Devenish +* Pedro Algarvio +* Randy Barlow +* Ronny Pfannschmidt +* Tomer Keren +* feuillemorte +* wim glenn + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 43fedebed..b13a071f6 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -100,10 +100,9 @@ If you want to write test code that works on Python 2.4 as well, you may also use two other ways to test for an expected exception:: pytest.raises(ExpectedException, func, *args, **kwargs) - pytest.raises(ExpectedException, "func(*args, **kwargs)") -both of which execute the specified function with args and kwargs and -asserts that the given ``ExpectedException`` is raised. The reporter will +which will execute the specified function with args and kwargs and +assert that the given ``ExpectedException`` is raised. The reporter will provide you with helpful output in case of failures such as *no exception* or *wrong exception*. diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 1e376f0d3..a40dfc223 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -68,8 +68,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a def test_function(record_property): record_property("example_key", 1) - record_xml_property - (Deprecated) use record_property. record_xml_attribute Add extra xml attributes to the tag for the calling test. The fixture is callable with ``(name, value)``, with value being diff --git a/doc/en/cache.rst b/doc/en/cache.rst index a0fa72db1..ba9d87a5f 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -215,7 +215,9 @@ If you run this command for the first time, you can see the print statement: > assert mydata == 23 E assert 42 == 23 - test_caching.py:14: AssertionError + test_caching.py:17: AssertionError + -------------------------- Captured stdout setup --------------------------- + running expensive computation... 1 failed in 0.12 seconds If you run it a second time the value will be retrieved from @@ -234,7 +236,7 @@ the cache and nothing will be printed: > assert mydata == 23 E assert 42 == 23 - test_caching.py:14: AssertionError + test_caching.py:17: AssertionError 1 failed in 0.12 seconds See the :ref:`cache-api` for more details. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 3398c92a2..f3240cec7 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -7,6 +7,11 @@ This page lists all pytest features that are currently deprecated or have been r The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives should be used instead. +.. contents:: + :depth: 3 + :local: + + Deprecated Features ------------------- @@ -14,24 +19,205 @@ Below is a complete list of all pytest features which are considered deprecated. :class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. -Internal classes accessed through ``Node`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``"message"`` parameter of ``pytest.raises`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 3.9 +.. deprecated:: 4.1 -Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances now issue -this warning:: +It is a common mistake to think this parameter will match the exception message, while in fact +it only serves to provide a custom message in case the ``pytest.raises`` check fails. To avoid this +mistake and because it is believed to be little used, pytest is deprecating it without providing +an alternative for the moment. - usage of Function.Module is deprecated, please use pytest.Module instead +If you have concerns about this, please comment on `issue #3974 `__. -Users should just ``import pytest`` and access those objects using the ``pytest`` module. -This has been documented as deprecated for years, but only now we are actually emitting deprecation warnings. +``pytest.config`` global +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 4.1 + +The ``pytest.config`` global object is deprecated. Instead use +``request.config`` (via the ``request`` fixture) or if you are a plugin author +use the ``pytest_configure(config)`` hook. + +.. _raises-warns-exec: + +``raises`` / ``warns`` with a string as the second argument +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 4.1 + +Use the context manager form of these instead. When necessary, invoke ``exec`` +directly. + +Example: + +.. code-block:: python + + pytest.raises(ZeroDivisionError, "1 / 0") + pytest.raises(SyntaxError, "a $ b") + + pytest.warns(DeprecationWarning, "my_function()") + pytest.warns(SyntaxWarning, "assert(1, 2)") + +Becomes: + +.. code-block:: python + + with pytest.raises(ZeroDivisionError): + 1 / 0 + with pytest.raises(SyntaxError): + exec("a $ b") # exec is required for invalid syntax + + with pytest.warns(DeprecationWarning): + my_function() + with pytest.warns(SyntaxWarning): + exec("assert(1, 2)") # exec is used to avoid a top-level warning + + + + + + +Result log (``--result-log``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 3.0 + +The ``--resultlog`` command line option has been deprecated: it is little used +and there are more modern and better alternatives, for example `pytest-tap `_. + +This feature will be effectively removed in pytest 4.0 as the team intends to include a better alternative in the core. + +If you have any concerns, please don't hesitate to `open an issue `__. + +Removed Features +---------------- + +As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after +an appropriate period of deprecation has passed. + +Using ``Class`` in custom Collectors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 4.0.* + +Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector`` +subclasses has been deprecated. Users instead should use ``pytest_pycollect_makeitem`` to customize node types during +collection. + +This issue should affect only advanced plugins who create new collection types, so if you see this warning +message please contact the authors so they can change the code. + + +marks in ``pytest.mark.parametrize`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 4.0.* + +Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated. For example: + +.. code-block:: python + + @pytest.mark.parametrize( + "a, b", + [ + (3, 9), + pytest.mark.xfail(reason="flaky")(6, 36), + (10, 100), + (20, 200), + (40, 400), + (50, 500), + ], + ) + def test_foo(a, b): + ... + +This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization +call. + +This was considered hard to read and understand, and also its implementation presented problems to the code preventing +further internal improvements in the marks architecture. + +To update the code, use ``pytest.param``: + +.. code-block:: python + + @pytest.mark.parametrize( + "a, b", + [ + (3, 9), + pytest.param(6, 36, marks=pytest.mark.xfail(reason="flaky")), + (10, 100), + (20, 200), + (40, 400), + (50, 500), + ], + ) + def test_foo(a, b): + ... + + +``pytest_funcarg__`` prefix +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 4.0.* + +In very early pytest versions fixtures could be defined using the ``pytest_funcarg__`` prefix: + +.. code-block:: python + + def pytest_funcarg__data(): + return SomeData() + +Switch over to the ``@pytest.fixture`` decorator: + +.. code-block:: python + + @pytest.fixture + def data(): + return SomeData() + + + +[pytest] section in setup.cfg files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 4.0.* + +``[pytest]`` sections in ``setup.cfg`` files should now be named ``[tool:pytest]`` +to avoid conflicts with other distutils commands. + + +Metafunc.addcall +~~~~~~~~~~~~~~~~ + +*Removed in version 4.0.* + +:meth:`_pytest.python.Metafunc.addcall` was a precursor to the current parametrized mechanism. Users should use +:meth:`_pytest.python.Metafunc.parametrize` instead. + +Example: + +.. code-block:: python + + def pytest_generate_tests(metafunc): + metafunc.addcall({"i": 1}, id="1") + metafunc.addcall({"i": 2}, id="2") + +Becomes: + +.. code-block:: python + + def pytest_generate_tests(metafunc): + metafunc.parametrize("i", [1, 2], ids=["1", "2"]) + ``cached_setup`` ~~~~~~~~~~~~~~~~ -.. deprecated:: 3.9 +*Removed in version 4.0.* ``request.cached_setup`` was the precursor of the setup/teardown mechanism available to fixtures. @@ -59,26 +245,21 @@ This should be updated to make use of standard fixture mechanisms: You can consult `funcarg comparison section in the docs `_ for more information. -This has been documented as deprecated for years, but only now we are actually emitting deprecation warnings. +pytest_plugins in non-top-level conftest files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Using ``Class`` in custom Collectors -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*Removed in version 4.0.* -.. deprecated:: 3.9 - -Using objects named ``"Class"`` as a way to customize the type of nodes that are collected in ``Collector`` -subclasses has been deprecated. Users instead should use ``pytest_pycollect_makeitem`` to customize node types during -collection. - -This issue should affect only advanced plugins who create new collection types, so if you see this warning -message please contact the authors so they can change the code. +Defining ``pytest_plugins`` is now deprecated in non-top-level conftest.py +files because they will activate referenced plugins *globally*, which is surprising because for all other pytest +features ``conftest.py`` files are only *active* for tests at or below it. ``Config.warn`` and ``Node.warn`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 3.8 +*Removed in version 4.0.* Those methods were part of the internal pytest warnings system, but since ``3.8`` pytest is using the builtin warning system for its own warnings, so those two functions are now deprecated. @@ -100,47 +281,57 @@ Becomes: * ``node.warn(PytestWarning("some message"))``: is now the **recommended** way to call this function. The warning instance must be a PytestWarning or subclass. -* ``node.warn("CI", "some message")``: this code/message form is now **deprecated** and should be converted to the warning instance form above. +* ``node.warn("CI", "some message")``: this code/message form has been **removed** and should be converted to the warning instance form above. +record_xml_property +~~~~~~~~~~~~~~~~~~~ -``pytest_namespace`` -~~~~~~~~~~~~~~~~~~~~ +*Removed in version 4.0.* -.. deprecated:: 3.7 +The ``record_xml_property`` fixture is now deprecated in favor of the more generic ``record_property``, which +can be used by other consumers (for example ``pytest-html``) to obtain custom information about the test run. -This hook is deprecated because it greatly complicates the pytest internals regarding configuration and initialization, making some -bug fixes and refactorings impossible. - -Example of usage: +This is just a matter of renaming the fixture as the API is the same: .. code-block:: python - class MySymbol: + def test_foo(record_xml_property): + ... + +Change to: + +.. code-block:: python + + def test_foo(record_property): ... - def pytest_namespace(): - return {"my_symbol": MySymbol()} +Passing command-line string to ``pytest.main()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*Removed in version 4.0.* -Plugin authors relying on this hook should instead require that users now import the plugin modules directly (with an appropriate public API). - -As a stopgap measure, plugin authors may still inject their names into pytest's namespace, usually during ``pytest_configure``: +Passing a command-line string to ``pytest.main()`` is deprecated: .. code-block:: python - import pytest + pytest.main("-v -s") + +Pass a list instead: + +.. code-block:: python + + pytest.main(["-v", "-s"]) - def pytest_configure(): - pytest.my_symbol = MySymbol() - +By passing a string, users expect that pytest will interpret that command-line using the shell rules they are working +on (for example ``bash`` or ``Powershell``), but this is very hard/impossible to do in a portable way. Calling fixtures directly ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 3.7 +*Removed in version 4.0.* Calling a fixture function directly, as opposed to request them in a test function, is deprecated. @@ -175,116 +366,27 @@ In those cases just request the function directly in the dependent fixture: cell.make_full() return cell -``Node.get_marker`` -~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 3.6 - -As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` is deprecated. See -:ref:`the documentation ` on tips on how to update your code. - - -record_xml_property -~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 3.5 - -The ``record_xml_property`` fixture is now deprecated in favor of the more generic ``record_property``, which -can be used by other consumers (for example ``pytest-html``) to obtain custom information about the test run. - -This is just a matter of renaming the fixture as the API is the same: +Alternatively if the fixture function is called multiple times inside a test (making it hard to apply the above pattern) or +if you would like to make minimal changes to the code, you can create a fixture which calls the original function together +with the ``name`` parameter: .. code-block:: python - def test_foo(record_xml_property): - ... - -Change to: - -.. code-block:: python - - def test_foo(record_property): - ... - -pytest_plugins in non-top-level conftest files -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 3.5 - -Defining ``pytest_plugins`` is now deprecated in non-top-level conftest.py -files because they will activate referenced plugins *globally*, which is surprising because for all other pytest -features ``conftest.py`` files are only *active* for tests at or below it. - -Metafunc.addcall -~~~~~~~~~~~~~~~~ - -.. deprecated:: 3.3 - -:meth:`_pytest.python.Metafunc.addcall` was a precursor to the current parametrized mechanism. Users should use -:meth:`_pytest.python.Metafunc.parametrize` instead. - -marks in ``pytest.mark.parametrize`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 3.2 - -Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated. For example: - -.. code-block:: python - - @pytest.mark.parametrize( - "a, b", [(3, 9), pytest.mark.xfail(reason="flaky")(6, 36), (10, 100)] - ) - def test_foo(a, b): - ... - -This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization -call. - -This was considered hard to read and understand, and also its implementation presented problems to the code preventing -further internal improvements in the marks architecture. - -To update the code, use ``pytest.param``: - -.. code-block:: python - - @pytest.mark.parametrize( - "a, b", - [(3, 9), pytest.param((6, 36), marks=pytest.mark.xfail(reason="flaky")), (10, 100)], - ) - def test_foo(a, b): - ... + def cell(): + return ... - -Passing command-line string to ``pytest.main()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 3.0 - -Passing a command-line string to ``pytest.main()`` is deprecated: - -.. code-block:: python - - pytest.main("-v -s") - -Pass a list instead: - -.. code-block:: python - - pytest.main(["-v", "-s"]) - - -By passing a string, users expect that pytest will interpret that command-line using the shell rules they are working -on (for example ``bash`` or ``Powershell``), but this is very hard/impossible to do in a portable way. + @pytest.fixture(name="cell") + def cell_fixture(): + return cell() ``yield`` tests ~~~~~~~~~~~~~~~ -.. deprecated:: 3.0 +*Removed in version 4.0.* -pytest supports ``yield``-style tests, where a test function actually ``yield`` functions and values +pytest supported ``yield``-style tests, where a test function actually ``yield`` functions and values that are then turned into proper test methods. Example: .. code-block:: python @@ -307,48 +409,53 @@ This form of test function doesn't support fixtures properly, and users should s def test_squared(x, y): assert x ** x == y +Internal classes accessed through ``Node`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``pytest_funcarg__`` prefix -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*Removed in version 4.0.* -.. deprecated:: 3.0 +Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances now issue +this warning:: -In very early pytest versions fixtures could be defined using the ``pytest_funcarg__`` prefix: + usage of Function.Module is deprecated, please use pytest.Module instead + +Users should just ``import pytest`` and access those objects using the ``pytest`` module. + +This has been documented as deprecated for years, but only now we are actually emitting deprecation warnings. + +``pytest_namespace`` +~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 4.0.* + +This hook is deprecated because it greatly complicates the pytest internals regarding configuration and initialization, making some +bug fixes and refactorings impossible. + +Example of usage: .. code-block:: python - def pytest_funcarg__data(): - return SomeData() + class MySymbol: + ... -Switch over to the ``@pytest.fixture`` decorator: + + def pytest_namespace(): + return {"my_symbol": MySymbol()} + + +Plugin authors relying on this hook should instead require that users now import the plugin modules directly (with an appropriate public API). + +As a stopgap measure, plugin authors may still inject their names into pytest's namespace, usually during ``pytest_configure``: .. code-block:: python - @pytest.fixture - def data(): - return SomeData() + import pytest -[pytest] section in setup.cfg files -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 3.0 + def pytest_configure(): + pytest.my_symbol = MySymbol() -``[pytest]`` sections in ``setup.cfg`` files should now be named ``[tool:pytest]`` -to avoid conflicts with other distutils commands. -Result log (``--result-log``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 3.0 - -The ``--resultlog`` command line option has been deprecated: it is little used -and there are more modern and better alternatives, for example `pytest-tap `_. - -Removed Features ----------------- - -As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after -an appropriate period of deprecation has passed. Reinterpretation mode (``--assert=reinterp``) @@ -384,3 +491,21 @@ Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points were never documented and a leftover from a pre-virtualenv era. These entry points also created broken entry points in wheels, so removing them also removes a source of confusion for users. + + +``Node.get_marker`` +~~~~~~~~~~~~~~~~~~~ + +*Removed in version 4.0* + +As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` is deprecated. See +:ref:`the documentation ` on tips on how to update your code. + + +``somefunction.markname`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 4.0* + +As part of a large :ref:`marker-revamp` we already deprecated using ``MarkInfo`` +the only correct way to get markers of an element is via ``node.iter_markers(name)``. diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index a9615c215..31a9f2577 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -98,6 +98,30 @@ class TestSpecialisedExplanations(object): text = "head " * 50 + "f" * 70 + "tail " * 20 assert "f" * 70 not in text + def test_eq_dataclass(self): + from dataclasses import dataclass + + @dataclass + class Foo(object): + a: int + b: str + + left = Foo(1, "b") + right = Foo(1, "c") + assert left == right + + def test_eq_attrs(self): + import attr + + @attr.s + class Foo(object): + a = attr.ib() + b = attr.ib() + + left = Foo(1, "b") + right = Foo(1, "c") + assert left == right + def test_attribute(): class Foo(object): @@ -141,11 +165,11 @@ def globf(x): class TestRaises(object): def test_raises(self): - s = "qwe" # NOQA - raises(TypeError, "int(s)") + s = "qwe" + raises(TypeError, int, s) def test_raises_doesnt(self): - raises(IOError, "int('3')") + raises(IOError, int, "3") def test_raise(self): raise ValueError("demo error") diff --git a/doc/en/example/assertion/test_failures.py b/doc/en/example/assertion/test_failures.py index 9ffe31664..30ebc72dc 100644 --- a/doc/en/example/assertion/test_failures.py +++ b/doc/en/example/assertion/test_failures.py @@ -9,5 +9,5 @@ def test_failure_demo_fails_properly(testdir): failure_demo.copy(target) failure_demo.copy(testdir.tmpdir.join(failure_demo.basename)) result = testdir.runpytest(target, syspathinsert=True) - result.stdout.fnmatch_lines(["*42 failed*"]) + result.stdout.fnmatch_lines(["*44 failed*"]) assert result.ret != 0 diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 5b96a33bf..eba8279f3 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -90,9 +90,9 @@ interesting to just look at the collection tree: platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collected 2 items - - - - + + + + ======================= no tests ran in 0.12 seconds ======================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 16e48878c..92756e492 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -147,15 +147,15 @@ objects, they are still using the default pytest representation: platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 8 items - - - - - - - - - + + + + + + + + + ======================= no tests ran in 0.12 seconds ======================= @@ -219,12 +219,12 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items - - - - - - + + + + + + ======================= no tests ran in 0.12 seconds ======================= @@ -285,9 +285,9 @@ Let's first see how it looks like at collection time: platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items - - - + + + ======================= no tests ran in 0.12 seconds ======================= @@ -350,8 +350,8 @@ The result of this test will be successful: platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - - + + ======================= no tests ran in 0.12 seconds ======================= @@ -388,7 +388,8 @@ parametrizer`_ but in a lot less code:: assert a == b def test_zerodivision(self, a, b): - pytest.raises(ZeroDivisionError, "a/b") + with pytest.raises(ZeroDivisionError): + a / b Our test generator looks up a class-level definition which specifies which argument sets to use for each test function. Let's run it: diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 3f1dd68ee..394924e2d 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -134,10 +134,10 @@ The test collection would look like this: platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 2 items - - - - + + + + ======================= no tests ran in 0.12 seconds ======================= @@ -189,11 +189,11 @@ You can always peek at the collection tree without running tests like this: platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 3 items - - - - - + + + + + ======================= no tests ran in 0.12 seconds ======================= diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index ffccdf77f..15d71caa0 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -15,9 +15,9 @@ get on the terminal - we are working on that): =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR/assertion, inifile: - collected 42 items + collected 44 items - failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF [100%] + failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF [100%] ================================= FAILURES ================================= ___________________________ test_generative[3-6] ___________________________ @@ -289,6 +289,48 @@ get on the terminal - we are working on that): E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ failure_demo.py:99: AssertionError + ______________ TestSpecialisedExplanations.test_eq_dataclass _______________ + + self = + + def test_eq_dataclass(self): + from dataclasses import dataclass + + @dataclass + class Foo(object): + a: int + b: str + + left = Foo(1, "b") + right = Foo(1, "c") + > assert left == right + E AssertionError: assert TestSpecialis...oo(a=1, b='b') == TestSpecialise...oo(a=1, b='c') + E Omitting 1 identical items, use -vv to show + E Differing attributes: + E b: 'b' != 'c' + + failure_demo.py:111: AssertionError + ________________ TestSpecialisedExplanations.test_eq_attrs _________________ + + self = + + def test_eq_attrs(self): + import attr + + @attr.s + class Foo(object): + a = attr.ib() + b = attr.ib() + + left = Foo(1, "b") + right = Foo(1, "c") + > assert left == right + E AssertionError: assert Foo(a=1, b='b') == Foo(a=1, b='c') + E Omitting 1 identical items, use -vv to show + E Differing attributes: + E b: 'b' != 'c' + + failure_demo.py:123: AssertionError ______________________________ test_attribute ______________________________ def test_attribute(): @@ -300,7 +342,7 @@ get on the terminal - we are working on that): E assert 1 == 2 E + where 1 = .Foo object at 0xdeadbeef>.b - failure_demo.py:107: AssertionError + failure_demo.py:131: AssertionError _________________________ test_attribute_instance __________________________ def test_attribute_instance(): @@ -312,7 +354,7 @@ get on the terminal - we are working on that): E + where 1 = .Foo object at 0xdeadbeef>.b E + where .Foo object at 0xdeadbeef> = .Foo'>() - failure_demo.py:114: AssertionError + failure_demo.py:138: AssertionError __________________________ test_attribute_failure __________________________ def test_attribute_failure(): @@ -325,7 +367,7 @@ get on the terminal - we are working on that): i = Foo() > assert i.b == 2 - failure_demo.py:125: + failure_demo.py:149: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = .Foo object at 0xdeadbeef> @@ -334,7 +376,7 @@ get on the terminal - we are working on that): > raise Exception("Failed to get attrib") E Exception: Failed to get attrib - failure_demo.py:120: Exception + failure_demo.py:144: Exception _________________________ test_attribute_multiple __________________________ def test_attribute_multiple(): @@ -351,31 +393,26 @@ get on the terminal - we are working on that): E + and 2 = .Bar object at 0xdeadbeef>.b E + where .Bar object at 0xdeadbeef> = .Bar'>() - failure_demo.py:135: AssertionError + failure_demo.py:159: AssertionError __________________________ TestRaises.test_raises __________________________ self = def test_raises(self): - s = "qwe" # NOQA - > raises(TypeError, "int(s)") + s = "qwe" + > raises(TypeError, int, s) + E ValueError: invalid literal for int() with base 10: 'qwe' - failure_demo.py:145: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - - > int(s) - E ValueError: invalid literal for int() with base 10: 'qwe' - - <0-codegen $REGENDOC_TMPDIR/assertion/failure_demo.py:145>:1: ValueError + failure_demo.py:169: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ self = def test_raises_doesnt(self): - > raises(IOError, "int('3')") + > raises(IOError, int, "3") E Failed: DID NOT RAISE - failure_demo.py:148: Failed + failure_demo.py:172: Failed __________________________ TestRaises.test_raise ___________________________ self = @@ -384,7 +421,7 @@ get on the terminal - we are working on that): > raise ValueError("demo error") E ValueError: demo error - failure_demo.py:151: ValueError + failure_demo.py:175: ValueError ________________________ TestRaises.test_tupleerror ________________________ self = @@ -393,7 +430,7 @@ get on the terminal - we are working on that): > a, b = [1] # NOQA E ValueError: not enough values to unpack (expected 2, got 1) - failure_demo.py:154: ValueError + failure_demo.py:178: ValueError ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ self = @@ -404,7 +441,7 @@ get on the terminal - we are working on that): > a, b = items.pop() E TypeError: 'int' object is not iterable - failure_demo.py:159: TypeError + failure_demo.py:183: TypeError --------------------------- Captured stdout call --------------------------- items is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ @@ -415,7 +452,7 @@ get on the terminal - we are working on that): > if namenotexi: # NOQA E NameError: name 'namenotexi' is not defined - failure_demo.py:162: NameError + failure_demo.py:186: NameError ____________________ test_dynamic_compile_shows_nicely _____________________ def test_dynamic_compile_shows_nicely(): @@ -430,14 +467,14 @@ get on the terminal - we are working on that): sys.modules[name] = module > module.foo() - failure_demo.py:180: + failure_demo.py:204: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ def foo(): > assert 1 == 0 E AssertionError - <2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:177>:2: AssertionError + <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:201>:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ self = @@ -451,7 +488,7 @@ get on the terminal - we are working on that): > somefunc(f(), g()) - failure_demo.py:191: + failure_demo.py:215: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ failure_demo.py:13: in somefunc otherfunc(x, y) @@ -473,7 +510,7 @@ get on the terminal - we are working on that): > a, b = items E ValueError: not enough values to unpack (expected 2, got 0) - failure_demo.py:195: ValueError + failure_demo.py:219: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ self = @@ -483,7 +520,7 @@ get on the terminal - we are working on that): > a, b = items E TypeError: 'int' object is not iterable - failure_demo.py:199: TypeError + failure_demo.py:223: TypeError ______________________ TestMoreErrors.test_startswith ______________________ self = @@ -496,7 +533,7 @@ get on the terminal - we are working on that): E + where False = ('456') E + where = '123'.startswith - failure_demo.py:204: AssertionError + failure_demo.py:228: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ self = @@ -515,7 +552,7 @@ get on the terminal - we are working on that): E + where '123' = .f at 0xdeadbeef>() E + and '456' = .g at 0xdeadbeef>() - failure_demo.py:213: AssertionError + failure_demo.py:237: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ self = @@ -526,7 +563,7 @@ get on the terminal - we are working on that): E + where False = isinstance(43, float) E + where 43 = globf(42) - failure_demo.py:216: AssertionError + failure_demo.py:240: AssertionError _______________________ TestMoreErrors.test_instance _______________________ self = @@ -537,7 +574,7 @@ get on the terminal - we are working on that): E assert 42 != 42 E + where 42 = .x - failure_demo.py:220: AssertionError + failure_demo.py:244: AssertionError _______________________ TestMoreErrors.test_compare ________________________ self = @@ -547,7 +584,7 @@ get on the terminal - we are working on that): E assert 11 < 5 E + where 11 = globf(10) - failure_demo.py:223: AssertionError + failure_demo.py:247: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ self = @@ -558,7 +595,7 @@ get on the terminal - we are working on that): > assert x == 0 E assert 1 == 0 - failure_demo.py:228: AssertionError + failure_demo.py:252: AssertionError ___________________ TestCustomAssertMsg.test_single_line ___________________ self = @@ -573,7 +610,7 @@ get on the terminal - we are working on that): E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:239: AssertionError + failure_demo.py:263: AssertionError ____________________ TestCustomAssertMsg.test_multiline ____________________ self = @@ -592,7 +629,7 @@ get on the terminal - we are working on that): E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:246: AssertionError + failure_demo.py:270: AssertionError ___________________ TestCustomAssertMsg.test_custom_repr ___________________ self = @@ -614,5 +651,5 @@ get on the terminal - we are working on that): E assert 1 == 2 E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a - failure_demo.py:259: AssertionError - ======================== 42 failed in 0.12 seconds ========================= + failure_demo.py:283: AssertionError + ======================== 44 failed in 0.12 seconds ========================= diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 95c0e6365..76a1ddc80 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -598,7 +598,7 @@ We can run this: file $REGENDOC_TMPDIR/b/test_error.py, line 1 def test_root(db): # no db here, will error out E fixture 'db' not found - > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, record_xml_property, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory + > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory > use 'pytest --fixtures [testpath]' for help on them. $REGENDOC_TMPDIR/b/test_error.py:1 diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 133901dde..4dd68f8e4 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -628,7 +628,7 @@ So let's just do another run: response, msg = smtp_connection.ehlo() assert response == 250 > assert b"smtp.gmail.com" in msg - E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8' + E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING' test_module.py:5: AssertionError -------------------------- Captured stdout setup --------------------------- @@ -703,19 +703,19 @@ Running the above tests results in the following test IDs being used: platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 10 items - - - - - - - - - - - - - + + + + + + + + + + + + + ======================= no tests ran in 0.12 seconds ======================= diff --git a/doc/en/reference.rst b/doc/en/reference.rst index da53e7fea..9305cbb95 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -618,7 +618,6 @@ Session related reporting hooks: .. autofunction:: pytest_terminal_summary .. autofunction:: pytest_fixture_setup .. autofunction:: pytest_fixture_post_finalizer -.. autofunction:: pytest_logwarning .. autofunction:: pytest_warning_captured And here is the central hook for reporting about @@ -725,13 +724,6 @@ MarkGenerator :members: -MarkInfo -~~~~~~~~ - -.. autoclass:: _pytest.mark.MarkInfo - :members: - - Mark ~~~~ diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 7e385288e..3ff6a0dd5 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -152,40 +152,77 @@ making it easy in large test suites to get a clear picture of all failures, skip Example: +.. code-block:: python + + # content of test_example.py + import pytest + + + @pytest.fixture + def error_fixture(): + assert 0 + + + def test_ok(): + print("ok") + + + def test_fail(): + assert 0 + + + def test_error(error_fixture): + pass + + + def test_skip(): + pytest.skip("skipping this test") + + + def test_xfail(): + pytest.xfail("xfailing this test") + + + @pytest.mark.xfail(reason="always xfail") + def test_xpass(): + pass + + .. code-block:: pytest $ pytest -ra =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: - collected 7 items + collected 6 items - test_examples.py ..FEsxX [100%] + test_example.py .FEsxX [100%] - ==================================== ERRORS ==================================== - _________________________ ERROR at setup of test_error _________________________ - file /Users/chainz/tmp/pytestratest/test_examples.py, line 17 - def test_error(unknown_fixture): - E fixture 'unknown_fixture' not found - > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, record_xml_property, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory - > use 'pytest --fixtures [testpath]' for help on them. + ================================== ERRORS ================================== + _______________________ ERROR at setup of test_error _______________________ - /Users/chainz/tmp/pytestratest/test_examples.py:17 - =================================== FAILURES =================================== - __________________________________ test_fail ___________________________________ + @pytest.fixture + def error_fixture(): + > assert 0 + E assert 0 + + test_example.py:6: AssertionError + ================================= FAILURES ================================= + ________________________________ test_fail _________________________________ def test_fail(): > assert 0 E assert 0 - test_examples.py:14: AssertionError - =========================== short test summary info ============================ - FAIL test_examples.py::test_fail - ERROR test_examples.py::test_error - SKIP [1] test_examples.py:21: Example - XFAIL test_examples.py::test_xfail - XPASS test_examples.py::test_xpass - = 1 failed, 2 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.07 seconds = + test_example.py:14: AssertionError + ========================= short test summary info ========================== + SKIP [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + XFAIL test_example.py::test_xfail + reason: xfailing this test + XPASS test_example.py::test_xpass always xfail + ERROR test_example.py::test_error + FAIL test_example.py::test_fail + 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes". @@ -208,22 +245,31 @@ More than one character can be used, so for example to only see failed and skipp =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: - collected 2 items + collected 6 items - test_examples.py Fs [100%] + test_example.py .FEsxX [100%] - =================================== FAILURES =================================== - __________________________________ test_fail ___________________________________ + ================================== ERRORS ================================== + _______________________ ERROR at setup of test_error _______________________ + + @pytest.fixture + def error_fixture(): + > assert 0 + E assert 0 + + test_example.py:6: AssertionError + ================================= FAILURES ================================= + ________________________________ test_fail _________________________________ def test_fail(): > assert 0 E assert 0 - test_examples.py:14: AssertionError - =========================== short test summary info ============================ - FAIL test_examples.py::test_fail - SKIP [1] test_examples.py:21: Example - ===================== 1 failed, 1 skipped in 0.09 seconds ====================== + test_example.py:14: AssertionError + ========================= short test summary info ========================== + FAIL test_example.py::test_fail + SKIP [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had captured output: @@ -234,18 +280,34 @@ captured output: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-4.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: - collected 2 items + collected 6 items - test_examples.py .. [100%] - =========================== short test summary info ============================ - PASSED test_examples.py::test_pass - PASSED test_examples.py::test_pass_with_output + test_example.py .FEsxX [100%] - ==================================== PASSES ==================================== - ____________________________ test_pass_with_output _____________________________ - ----------------------------- Captured stdout call ----------------------------- - Passing test - =========================== 2 passed in 0.04 seconds =========================== + ================================== ERRORS ================================== + _______________________ ERROR at setup of test_error _______________________ + + @pytest.fixture + def error_fixture(): + > assert 0 + E assert 0 + + test_example.py:6: AssertionError + ================================= FAILURES ================================= + ________________________________ test_fail _________________________________ + + def test_fail(): + > assert 0 + E assert 0 + + test_example.py:14: AssertionError + ========================= short test summary info ========================== + PASSED test_example.py::test_ok + ================================== PASSES ================================== + _________________________________ test_ok __________________________________ + --------------------------- Captured stdout call --------------------------- + ok + 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds .. _pdb-option: @@ -354,6 +416,20 @@ To set the name of the root test suite xml item, you can configure the ``junit_s [pytest] junit_suite_name = my_suite +.. versionadded:: 4.0 + +JUnit XML specification seems to indicate that ``"time"`` attribute +should report total test execution times, including setup and teardown +(`1 `_, `2 +`_). +It is the default pytest behavior. To report just call durations +instead, configure the ``junit_duration_report`` option like this: + +.. code-block:: ini + + [pytest] + junit_duration_report = call + .. _record_property example: record_property @@ -543,14 +619,10 @@ Creating resultlog format files .. deprecated:: 3.0 - This option is rarely used and is scheduled for removal in 4.0. + This option is rarely used and is scheduled for removal in 5.0. - An alternative for users which still need similar functionality is to use the - `pytest-tap `_ plugin which provides - a stream of test data. - - If you have any concerns, please don't hesitate to - `open an issue `_. + See `the deprecation docs `__ + for more information. To create plain-text machine-readable result files you can issue:: @@ -621,8 +693,25 @@ Running it will show that ``MyPlugin`` was added and its hook was invoked:: $ python myinvoke.py - . [100%]*** test run reporting finishing + .FEsxX. [100%]*** test run reporting finishing + ================================== ERRORS ================================== + _______________________ ERROR at setup of test_error _______________________ + + @pytest.fixture + def error_fixture(): + > assert 0 + E assert 0 + + test_example.py:6: AssertionError + ================================= FAILURES ================================= + ________________________________ test_fail _________________________________ + + def test_fail(): + > assert 0 + E assert 0 + + test_example.py:14: AssertionError .. note:: diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 26973c4e1..1b49fe75b 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -391,44 +391,85 @@ co_equal = compile( ) +@attr.s(repr=False) class ExceptionInfo(object): """ wraps sys.exc_info() objects and offers help for navigating the traceback. """ - _striptext = "" _assert_start_repr = ( "AssertionError(u'assert " if _PY2 else "AssertionError('assert " ) - def __init__(self, tup=None, exprinfo=None): - import _pytest._code + _excinfo = attr.ib() + _striptext = attr.ib(default="") + _traceback = attr.ib(default=None) - if tup is None: - tup = sys.exc_info() - if exprinfo is None and isinstance(tup[1], AssertionError): - exprinfo = getattr(tup[1], "msg", None) - if exprinfo is None: - exprinfo = py.io.saferepr(tup[1]) - if exprinfo and exprinfo.startswith(self._assert_start_repr): - self._striptext = "AssertionError: " - self._excinfo = tup - #: the exception class - self.type = tup[0] - #: the exception instance - self.value = tup[1] - #: the exception raw traceback - self.tb = tup[2] - #: the exception type name - self.typename = self.type.__name__ - #: the exception traceback (_pytest._code.Traceback instance) - self.traceback = _pytest._code.Traceback(self.tb, excinfo=ref(self)) + @classmethod + def from_current(cls, exprinfo=None): + """returns an ExceptionInfo matching the current traceback + + .. warning:: + + Experimental API + + + :param exprinfo: a text string helping to determine if we should + strip ``AssertionError`` from the output, defaults + to the exception message/``__str__()`` + """ + tup = sys.exc_info() + _striptext = "" + if exprinfo is None and isinstance(tup[1], AssertionError): + exprinfo = getattr(tup[1], "msg", None) + if exprinfo is None: + exprinfo = py.io.saferepr(tup[1]) + if exprinfo and exprinfo.startswith(cls._assert_start_repr): + _striptext = "AssertionError: " + + return cls(tup, _striptext) + + @classmethod + def for_later(cls): + """return an unfilled ExceptionInfo + """ + return cls(None) + + @property + def type(self): + """the exception class""" + return self._excinfo[0] + + @property + def value(self): + """the exception value""" + return self._excinfo[1] + + @property + def tb(self): + """the exception raw traceback""" + return self._excinfo[2] + + @property + def typename(self): + """the type name of the exception""" + return self.type.__name__ + + @property + def traceback(self): + """the traceback""" + if self._traceback is None: + self._traceback = Traceback(self.tb, excinfo=ref(self)) + return self._traceback + + @traceback.setter + def traceback(self, value): + self._traceback = value def __repr__(self): - try: - return "" % (self.typename, len(self.traceback)) - except AttributeError: - return "" + if self._excinfo is None: + return "" + return "" % (self.typename, len(self.traceback)) def exconly(self, tryshort=False): """ return the exception as a string @@ -516,13 +557,11 @@ class ExceptionInfo(object): return fmt.repr_excinfo(self) def __str__(self): - try: - entry = self.traceback[-1] - except AttributeError: + if self._excinfo is None: return repr(self) - else: - loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) - return str(loc) + entry = self.traceback[-1] + loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) + return str(loc) def __unicode__(self): entry = self.traceback[-1] diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index d1231b774..1d2c27ed1 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -51,6 +51,19 @@ else: return ast.Call(a, b, c, None, None) +def ast_Call_helper(func_name, *args, **kwargs): + """ + func_name: str + args: Iterable[ast.expr] + kwargs: Dict[str,ast.expr] + """ + return ast.Call( + ast.Name(func_name, ast.Load()), + list(args), + [ast.keyword(key, val) for key, val in kwargs.items()], + ) + + class AssertionRewritingHook(object): """PEP302 Import hook which rewrites asserts.""" @@ -265,11 +278,11 @@ class AssertionRewritingHook(object): def _warn_already_imported(self, name): from _pytest.warning_types import PytestWarning - from _pytest.warnings import _issue_config_warning + from _pytest.warnings import _issue_warning_captured - _issue_config_warning( + _issue_warning_captured( PytestWarning("Module already imported so cannot be rewritten: %s" % name), - self.config, + self.config.hook, stacklevel=5, ) @@ -828,6 +841,13 @@ class AssertionRewriter(ast.NodeVisitor): self.push_format_context() # Rewrite assert into a bunch of statements. top_condition, explanation = self.visit(assert_.test) + # If in a test module, check if directly asserting None, in order to warn [Issue #3191] + if self.module_path is not None: + self.statements.append( + self.warn_about_none_ast( + top_condition, module_path=self.module_path, lineno=assert_.lineno + ) + ) # Create failure message. body = self.on_failure negation = ast.UnaryOp(ast.Not(), top_condition) @@ -858,6 +878,33 @@ class AssertionRewriter(ast.NodeVisitor): set_location(stmt, assert_.lineno, assert_.col_offset) return self.statements + def warn_about_none_ast(self, node, module_path, lineno): + """ + Returns an AST issuing a warning if the value of node is `None`. + This is used to warn the user when asserting a function that asserts + internally already. + See issue #3191 for more details. + """ + + # Using parse because it is different between py2 and py3. + AST_NONE = ast.parse("None").body[0].value + val_is_none = ast.Compare(node, [ast.Is()], [AST_NONE]) + send_warning = ast.parse( + """ +from _pytest.warning_types import PytestWarning +from warnings import warn_explicit +warn_explicit( + PytestWarning('asserting the value None, please use "assert is None"'), + category=None, + filename={filename!r}, + lineno={lineno}, +) + """.format( + filename=module_path.strpath, lineno=lineno + ) + ).body + return ast.If(val_is_none, send_warning, []) + def visit_Name(self, name): # Display the repr of the name if it's a local variable or # _should_repr_global_name() thinks it's acceptable. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 451e45495..cb220def3 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -122,6 +122,12 @@ def assertrepr_compare(config, op, left, right): def isset(x): return isinstance(x, (set, frozenset)) + def isdatacls(obj): + return getattr(obj, "__dataclass_fields__", None) is not None + + def isattrs(obj): + return getattr(obj, "__attrs_attrs__", None) is not None + def isiterable(obj): try: iter(obj) @@ -142,6 +148,9 @@ def assertrepr_compare(config, op, left, right): explanation = _compare_eq_set(left, right, verbose) elif isdict(left) and isdict(right): explanation = _compare_eq_dict(left, right, verbose) + elif type(left) == type(right) and (isdatacls(left) or isattrs(left)): + type_fn = (isdatacls, isattrs) + explanation = _compare_eq_cls(left, right, verbose, type_fn) if isiterable(left) and isiterable(right): expl = _compare_eq_iterable(left, right, verbose) if explanation is not None: @@ -155,7 +164,7 @@ def assertrepr_compare(config, op, left, right): explanation = [ u"(pytest_assertion plugin: representation of details failed. " u"Probably an object has a faulty __repr__.)", - six.text_type(_pytest._code.ExceptionInfo()), + six.text_type(_pytest._code.ExceptionInfo.from_current()), ] if not explanation: @@ -315,6 +324,38 @@ def _compare_eq_dict(left, right, verbose=False): return explanation +def _compare_eq_cls(left, right, verbose, type_fns): + isdatacls, isattrs = type_fns + if isdatacls(left): + all_fields = left.__dataclass_fields__ + fields_to_check = [field for field, info in all_fields.items() if info.compare] + elif isattrs(left): + all_fields = left.__attrs_attrs__ + fields_to_check = [field.name for field in all_fields if field.cmp] + + same = [] + diff = [] + for field in fields_to_check: + if getattr(left, field) == getattr(right, field): + same.append(field) + else: + diff.append(field) + + explanation = [] + if same and verbose < 2: + explanation.append(u"Omitting %s identical items, use -vv to show" % len(same)) + elif same: + explanation += [u"Matching attributes:"] + explanation += pprint.pformat(same).splitlines() + if diff: + explanation += [u"Differing attributes:"] + for field in diff: + explanation += [ + (u"%s: %r != %r") % (field, getattr(left, field), getattr(right, field)) + ] + return explanation + + def _notin_text(term, text, verbose=False): index = text.find(term) head = text[:index] diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 22ce578fc..87b2e5426 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -33,6 +33,13 @@ which provides the `--lf` and `--ff` options, as well as the `cache` fixture. See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information. """ +CACHEDIR_TAG_CONTENT = b"""\ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by pytest. +# For information about cache directory tags, see: +# http://www.bford.info/cachedir/spec.html +""" + @attr.s class Cache(object): @@ -52,12 +59,12 @@ class Cache(object): return resolve_from_str(config.getini("cache_dir"), config.rootdir) def warn(self, fmt, **args): - from _pytest.warnings import _issue_config_warning + from _pytest.warnings import _issue_warning_captured from _pytest.warning_types import PytestWarning - _issue_config_warning( + _issue_warning_captured( PytestWarning(fmt.format(**args) if args else fmt), - self._config, + self._config.hook, stacklevel=3, ) @@ -140,6 +147,10 @@ class Cache(object): msg = u"# Created by pytest automatically.\n*" gitignore_path.write_text(msg, encoding="UTF-8") + cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG") + if not cachedir_tag_path.is_file(): + cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT) + class LFPlugin(object): """ Plugin which implements the --lf (run last-failing) option """ diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 376b4f87b..533690949 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -773,9 +773,9 @@ def _py36_windowsconsoleio_workaround(stream): f.line_buffering, ) - sys.__stdin__ = sys.stdin = _reopen_stdio(sys.stdin, "rb") - sys.__stdout__ = sys.stdout = _reopen_stdio(sys.stdout, "wb") - sys.__stderr__ = sys.stderr = _reopen_stdio(sys.stderr, "wb") + sys.stdin = _reopen_stdio(sys.stdin, "rb") + sys.stdout = _reopen_stdio(sys.stdout, "wb") + sys.stderr = _reopen_stdio(sys.stderr, "wb") def _attempt_to_close_capture_file(f): diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index ead9ffd8d..ff027f308 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -45,11 +45,11 @@ MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError" if _PY3: from collections.abc import MutableMapping as MappingMixin - from collections.abc import Mapping, Sequence + from collections.abc import Iterable, Mapping, Sequence, Sized else: # those raise DeprecationWarnings in Python >=3.7 from collections import MutableMapping as MappingMixin # noqa - from collections import Mapping, Sequence # noqa + from collections import Iterable, Mapping, Sequence, Sized # noqa if sys.version_info >= (3, 4): @@ -182,6 +182,18 @@ def get_default_arg_names(function): ) +_non_printable_ascii_translate_table = { + i: u"\\x{:02x}".format(i) for i in range(128) if i not in range(32, 127) +} +_non_printable_ascii_translate_table.update( + {ord("\t"): u"\\t", ord("\r"): u"\\r", ord("\n"): u"\\n"} +) + + +def _translate_non_printable(s): + return s.translate(_non_printable_ascii_translate_table) + + if _PY3: STRING_TYPES = bytes, str UNICODE_TYPES = six.text_type @@ -221,9 +233,10 @@ if _PY3: """ if isinstance(val, bytes): - return _bytes_to_ascii(val) + ret = _bytes_to_ascii(val) else: - return val.encode("unicode_escape").decode("ascii") + ret = val.encode("unicode_escape").decode("ascii") + return _translate_non_printable(ret) else: @@ -241,11 +254,12 @@ else: """ if isinstance(val, bytes): try: - return val.encode("ascii") + ret = val.decode("ascii") except UnicodeDecodeError: - return val.encode("string-escape") + ret = val.encode("string-escape").decode("ascii") else: - return val.encode("unicode-escape") + ret = val.encode("unicode-escape").decode("ascii") + return _translate_non_printable(ret) class _PytestWrapper(object): @@ -375,7 +389,6 @@ else: COLLECT_FAKEMODULE_ATTRIBUTES = ( "Collector", "Module", - "Generator", "Function", "Instance", "Session", diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index f70c91590..051eda79d 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -26,11 +26,14 @@ from .exceptions import PrintHelp from .exceptions import UsageError from .findpaths import determine_setup from .findpaths import exists +from _pytest import deprecated from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest.compat import lru_cache from _pytest.compat import safe_str +from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.warning_types import PytestWarning hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") @@ -173,12 +176,9 @@ def _prepareconfig(args=None, plugins=None): elif isinstance(args, py.path.local): args = [str(args)] elif not isinstance(args, (tuple, list)): - if not isinstance(args, str): - raise ValueError("not a string or argument list: %r" % (args,)) - args = shlex.split(args, posix=sys.platform != "win32") - from _pytest import deprecated + msg = "`args` parameter expected to be a list or tuple of strings, got: {!r} (type: {})" + raise TypeError(msg.format(args, type(args))) - warning = deprecated.MAIN_STR_ARGS config = get_config() pluginmanager = config.pluginmanager try: @@ -189,9 +189,9 @@ def _prepareconfig(args=None, plugins=None): else: pluginmanager.register(plugin) if warning: - from _pytest.warnings import _issue_config_warning + from _pytest.warnings import _issue_warning_captured - _issue_config_warning(warning, config=config, stacklevel=4) + _issue_warning_captured(warning, hook=config.hook, stacklevel=4) return pluginmanager.hook.pytest_cmdline_parse( pluginmanager=pluginmanager, args=args ) @@ -245,14 +245,7 @@ class PytestPluginManager(PluginManager): Use :py:meth:`pluggy.PluginManager.add_hookspecs ` instead. """ - warning = dict( - code="I2", - fslocation=_pytest._code.getfslineno(sys._getframe(1)), - nodeid=None, - message="use pluginmanager.add_hookspecs instead of " - "deprecated addhooks() method.", - ) - self._warn(warning) + warnings.warn(deprecated.PLUGIN_MANAGER_ADDHOOKS, stacklevel=2) return self.add_hookspecs(module_or_class) def parse_hookimpl_opts(self, plugin, name): @@ -261,8 +254,8 @@ class PytestPluginManager(PluginManager): # (see issue #1073) if not name.startswith("pytest_"): return - # ignore some historic special names which can not be hooks anyway - if name == "pytest_plugins" or name.startswith("pytest_funcarg__"): + # ignore names which can not be hooks + if name == "pytest_plugins": return method = getattr(plugin, name) @@ -275,10 +268,14 @@ class PytestPluginManager(PluginManager): # collect unmarked hooks as long as they have the `pytest_' prefix if opts is None and name.startswith("pytest_"): opts = {} - if opts is not None: + # TODO: DeprecationWarning, people should use hookimpl + # https://github.com/pytest-dev/pytest/issues/4562 + known_marks = {m.name for m in getattr(method, "pytestmark", [])} + for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"): - opts.setdefault(name, hasattr(method, name)) + + opts.setdefault(name, hasattr(method, name) or name in known_marks) return opts def parse_hookspec_opts(self, module_or_class, name): @@ -287,19 +284,27 @@ class PytestPluginManager(PluginManager): ) if opts is None: method = getattr(module_or_class, name) + if name.startswith("pytest_"): + # todo: deprecate hookspec hacks + # https://github.com/pytest-dev/pytest/issues/4562 + known_marks = {m.name for m in getattr(method, "pytestmark", [])} opts = { - "firstresult": hasattr(method, "firstresult"), - "historic": hasattr(method, "historic"), + "firstresult": hasattr(method, "firstresult") + or "firstresult" in known_marks, + "historic": hasattr(method, "historic") + or "historic" in known_marks, } return opts def register(self, plugin, name=None): if name in ["pytest_catchlog", "pytest_capturelog"]: - self._warn( - "{} plugin has been merged into the core, " - "please remove it from your requirements.".format( - name.replace("_", "-") + warnings.warn( + PytestWarning( + "{} plugin has been merged into the core, " + "please remove it from your requirements.".format( + name.replace("_", "-") + ) ) ) return @@ -336,14 +341,6 @@ class PytestPluginManager(PluginManager): ) self._configured = True - def _warn(self, message): - kwargs = ( - message - if isinstance(message, dict) - else {"code": "I1", "message": message, "fslocation": None, "nodeid": None} - ) - self.hook.pytest_logwarning.call_historic(kwargs=kwargs) - # # internal API for local conftest plugin handling # @@ -443,11 +440,11 @@ class PytestPluginManager(PluginManager): PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST ) - warnings.warn_explicit( - PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST, - category=None, - filename=str(conftestpath), - lineno=0, + fail( + PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST.format( + conftestpath, self._confcutdir + ), + pytrace=False, ) except Exception: raise ConftestImportFailure(conftestpath, sys.exc_info()) @@ -470,9 +467,20 @@ class PytestPluginManager(PluginManager): # def consider_preparse(self, args): - for opt1, opt2 in zip(args, args[1:]): - if opt1 == "-p": - self.consider_pluginarg(opt2) + i = 0 + n = len(args) + while i < n: + opt = args[i] + i += 1 + if isinstance(opt, six.string_types): + if opt == "-p": + parg = args[i] + i += 1 + elif opt.startswith("-p"): + parg = opt[2:] + else: + continue + self.consider_pluginarg(parg) def consider_pluginarg(self, arg): if arg.startswith("no:"): @@ -507,7 +515,7 @@ class PytestPluginManager(PluginManager): # "terminal" or "capture". Those plugins are registered under their # basename for historic purposes but must be imported with the # _pytest prefix. - assert isinstance(modname, (six.text_type, str)), ( + assert isinstance(modname, six.string_types), ( "module name as text required, got %r" % modname ) modname = str(modname) @@ -531,7 +539,13 @@ class PytestPluginManager(PluginManager): six.reraise(new_exc_type, new_exc, sys.exc_info()[2]) except Skipped as e: - self._warn("skipped plugin %r: %s" % ((modname, e.msg))) + from _pytest.warnings import _issue_warning_captured + + _issue_warning_captured( + PytestWarning("skipped plugin %r: %s" % (modname, e.msg)), + self.hook, + stacklevel=1, + ) else: mod = sys.modules[importspec] self.register(mod, modname) @@ -606,16 +620,9 @@ class Config(object): self._override_ini = () self._opt2dest = {} self._cleanup = [] - self._warn = self.pluginmanager._warn self.pluginmanager.register(self, "pytestconfig") self._configured = False - - def do_setns(dic): - import pytest - - setns(pytest, dic) - - self.hook.pytest_namespace.call_historic(do_setns, {}) + self.invocation_dir = py.path.local() self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) def add_cleanup(self, func): @@ -637,36 +644,6 @@ class Config(object): fin = self._cleanup.pop() fin() - def warn(self, code, message, fslocation=None, nodeid=None): - """ - .. deprecated:: 3.8 - - Use :py:func:`warnings.warn` or :py:func:`warnings.warn_explicit` directly instead. - - Generate a warning for this test session. - """ - from _pytest.warning_types import RemovedInPytest4Warning - - if isinstance(fslocation, (tuple, list)) and len(fslocation) > 2: - filename, lineno = fslocation[:2] - else: - filename = "unknown file" - lineno = 0 - msg = "config.warn has been deprecated, use warnings.warn instead" - if nodeid: - msg = "{}: {}".format(nodeid, msg) - warnings.warn_explicit( - RemovedInPytest4Warning(msg), - category=None, - filename=filename, - lineno=lineno, - ) - self.hook.pytest_logwarning.call_historic( - kwargs=dict( - code=code, message=message, fslocation=fslocation, nodeid=nodeid - ) - ) - def get_terminal_writer(self): return self.pluginmanager.get_plugin("terminalreporter")._tw @@ -731,7 +708,6 @@ class Config(object): self.rootdir, self.inifile, self.inicfg = r self._parser.extra_info["rootdir"] = self.rootdir self._parser.extra_info["inifile"] = self.inifile - self.invocation_dir = py.path.local() self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("minversion", "minimally required pytest version") self._override_ini = ns.override_ini or () @@ -822,7 +798,15 @@ class Config(object): if ns.help or ns.version: # we don't want to prevent --help/--version to work # so just let is pass and print a warning at the end - self._warn("could not load initial conftests (%s)\n" % e.path) + from _pytest.warnings import _issue_warning_captured + + _issue_warning_captured( + PytestWarning( + "could not load initial conftests: {}".format(e.path) + ), + self.hook, + stacklevel=2, + ) else: raise diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 5b8306dda..51f708335 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -18,6 +18,8 @@ class Parser(object): there's an error processing the command line arguments. """ + prog = None + def __init__(self, usage=None, processopt=None): self._anonymous = OptionGroup("custom options", parser=self) self._groups = [] @@ -82,7 +84,7 @@ class Parser(object): def _getparser(self): from _pytest._argcomplete import filescompleter - optparser = MyOptionParser(self, self.extra_info) + optparser = MyOptionParser(self, self.extra_info, prog=self.prog) groups = self._groups + [self._anonymous] for group in groups: if group.options: @@ -319,12 +321,13 @@ class OptionGroup(object): class MyOptionParser(argparse.ArgumentParser): - def __init__(self, parser, extra_info=None): + def __init__(self, parser, extra_info=None, prog=None): if not extra_info: extra_info = {} self._parser = parser argparse.ArgumentParser.__init__( self, + prog=prog, usage=parser._usage, add_help=False, formatter_class=DropShorterLongHelpFormatter, diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index a9d674e77..a0f16134d 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -3,6 +3,7 @@ import os import py from .exceptions import UsageError +from _pytest.outcomes import fail def exists(path, ignore=EnvironmentError): @@ -34,15 +35,10 @@ def getcfg(args, config=None): iniconfig = py.iniconfig.IniConfig(p) if "pytest" in iniconfig.sections: if inibasename == "setup.cfg" and config is not None: - from _pytest.warnings import _issue_config_warning - from _pytest.warning_types import RemovedInPytest4Warning - _issue_config_warning( - RemovedInPytest4Warning( - CFG_PYTEST_SECTION.format(filename=inibasename) - ), - config=config, - stacklevel=2, + fail( + CFG_PYTEST_SECTION.format(filename=inibasename), + pytrace=False, ) return base, p, iniconfig["pytest"] if ( @@ -112,40 +108,41 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): inicfg = iniconfig[section] if is_cfg_file and section == "pytest" and config is not None: from _pytest.deprecated import CFG_PYTEST_SECTION - from _pytest.warnings import _issue_config_warning - # TODO: [pytest] section in *.cfg files is deprecated. Need refactoring once - # the deprecation expires. - _issue_config_warning( - CFG_PYTEST_SECTION.format(filename=str(inifile)), - config, - stacklevel=2, + fail( + CFG_PYTEST_SECTION.format(filename=str(inifile)), pytrace=False ) break except KeyError: inicfg = None - rootdir = get_common_ancestor(dirs) + if rootdir_cmd_arg is None: + rootdir = get_common_ancestor(dirs) else: ancestor = get_common_ancestor(dirs) rootdir, inifile, inicfg = getcfg([ancestor], config=config) - if rootdir is None: - for rootdir in ancestor.parts(reverse=True): - if rootdir.join("setup.py").exists(): + if rootdir is None and rootdir_cmd_arg is None: + for possible_rootdir in ancestor.parts(reverse=True): + if possible_rootdir.join("setup.py").exists(): + rootdir = possible_rootdir break else: - rootdir, inifile, inicfg = getcfg(dirs, config=config) + if dirs != [ancestor]: + rootdir, inifile, inicfg = getcfg(dirs, config=config) if rootdir is None: - rootdir = get_common_ancestor([py.path.local(), ancestor]) + if config is not None: + cwd = config.invocation_dir + else: + cwd = py.path.local() + rootdir = get_common_ancestor([cwd, ancestor]) is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" if is_fs_root: rootdir = ancestor if rootdir_cmd_arg: - rootdir_abs_path = py.path.local(os.path.expandvars(rootdir_cmd_arg)) - if not os.path.isdir(str(rootdir_abs_path)): + rootdir = py.path.local(os.path.expandvars(rootdir_cmd_arg)) + if not rootdir.isdir(): raise UsageError( "Directory '{}' not found. Check your '--rootdir' option.".format( - rootdir_abs_path + rootdir ) ) - rootdir = rootdir_abs_path return rootdir, inifile, inicfg or {} diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index fe54d4939..adf9d0e54 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -77,18 +77,21 @@ class pytestPDB(object): _saved = [] @classmethod - def set_trace(cls, set_break=True): - """ invoke PDB set_trace debugging, dropping any IO capturing. """ + def _init_pdb(cls, *args, **kwargs): + """ Initialize PDB debugging, dropping any IO capturing. """ import _pytest.config - frame = sys._getframe().f_back if cls._pluginmanager is not None: capman = cls._pluginmanager.getplugin("capturemanager") if capman: capman.suspend_global_capture(in_=True) tw = _pytest.config.create_terminal_writer(cls._config) tw.line() - if capman and capman.is_globally_capturing(): + # Handle header similar to pdb.set_trace in py37+. + header = kwargs.pop("header", None) + if header is not None: + tw.sep(">", header) + elif capman and capman.is_globally_capturing(): tw.sep(">", "PDB set_trace (IO-capturing turned off)") else: tw.sep(">", "PDB set_trace") @@ -129,13 +132,18 @@ class pytestPDB(object): self._pytest_capman.suspend_global_capture(in_=True) return ret - _pdb = _PdbWrapper() + _pdb = _PdbWrapper(**kwargs) cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb) else: - _pdb = cls._pdb_cls() + _pdb = cls._pdb_cls(**kwargs) + return _pdb - if set_break: - _pdb.set_trace(frame) + @classmethod + def set_trace(cls, *args, **kwargs): + """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing.""" + frame = sys._getframe().f_back + _pdb = cls._init_pdb(*args, **kwargs) + _pdb.set_trace(frame) class PdbInvoke(object): @@ -161,9 +169,9 @@ class PdbTrace(object): def _test_pytest_function(pyfuncitem): - pytestPDB.set_trace(set_break=False) + _pdb = pytestPDB._init_pdb() testfunction = pyfuncitem.obj - pyfuncitem.obj = pdb.runcall + pyfuncitem.obj = _pdb.runcall if pyfuncitem._isyieldedfunction(): arg_list = list(pyfuncitem._args) arg_list.insert(0, testfunction) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 8d7a17bca..494a453b6 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -14,66 +14,38 @@ from __future__ import print_function from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import RemovedInPytest4Warning -from _pytest.warning_types import UnformattedWarning -MAIN_STR_ARGS = RemovedInPytest4Warning( - "passing a string to pytest.main() is deprecated, " - "pass a list of arguments instead." -) +YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored" -YIELD_TESTS = RemovedInPytest4Warning( - "yield tests are deprecated, and scheduled to be removed in pytest 4.0" -) -CACHED_SETUP = RemovedInPytest4Warning( - "cached_setup is deprecated and will be removed in a future release. " - "Use standard fixture functions instead." -) - -COMPAT_PROPERTY = UnformattedWarning( - RemovedInPytest4Warning, - "usage of {owner}.{name} is deprecated, please use pytest.{name} instead", -) - -CUSTOM_CLASS = UnformattedWarning( - RemovedInPytest4Warning, - 'use of special named "{name}" objects in collectors of type "{type_name}" to ' - "customize the created nodes is deprecated. " - "Use pytest_pycollect_makeitem(...) to create custom " - "collection nodes instead.", -) - -FUNCARG_PREFIX = UnformattedWarning( - RemovedInPytest4Warning, - '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated ' - "and scheduled to be removed in pytest 4.0. " - "Please remove the prefix and use the @pytest.fixture decorator instead.", -) - -FIXTURE_FUNCTION_CALL = UnformattedWarning( - RemovedInPytest4Warning, - 'Fixture "{name}" called directly. Fixtures are not meant to be called directly, ' - "are created automatically when test functions request them as parameters. " - "See https://docs.pytest.org/en/latest/fixture.html for more information.", +FIXTURE_FUNCTION_CALL = ( + 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' + "but are created automatically when test functions request them as parameters.\n" + "See https://docs.pytest.org/en/latest/fixture.html for more information about fixtures, and\n" + "https://docs.pytest.org/en/latest/deprecations.html#calling-fixtures-directly about how to update your code." ) FIXTURE_NAMED_REQUEST = PytestDeprecationWarning( "'request' is a reserved name for fixtures and will raise an error in future versions" ) -CFG_PYTEST_SECTION = UnformattedWarning( - RemovedInPytest4Warning, - "[pytest] section in {filename} files is deprecated, use [tool:pytest] instead.", -) +CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." GETFUNCARGVALUE = RemovedInPytest4Warning( "getfuncargvalue is deprecated, use getfixturevalue" ) -RESULT_LOG = RemovedInPytest4Warning( - "--result-log is deprecated and scheduled for removal in pytest 4.0.\n" - "See https://docs.pytest.org/en/latest/usage.html#creating-resultlog-format-files for more information." +RAISES_MESSAGE_PARAMETER = PytestDeprecationWarning( + "The 'message' parameter is deprecated.\n" + "(did you mean to use `match='some regex'` to check the exception message?)\n" + "Please comment on https://github.com/pytest-dev/pytest/issues/3974 " + "if you have concerns about removal of this parameter." +) + +RESULT_LOG = PytestDeprecationWarning( + "--result-log is deprecated and scheduled for removal in pytest 5.0.\n" + "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." ) MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning( @@ -82,42 +54,36 @@ MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning( "Docs: https://docs.pytest.org/en/latest/mark.html#updating-code" ) -MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning( - "Applying marks directly to parameters is deprecated," - " please use pytest.param(..., marks=...) instead.\n" - "For more details, see: https://docs.pytest.org/en/latest/parametrize.html" +RAISES_EXEC = PytestDeprecationWarning( + "raises(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly\n\n" + "See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec" +) +WARNS_EXEC = PytestDeprecationWarning( + "warns(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly.\n\n" + "See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec" ) -NODE_WARN = RemovedInPytest4Warning( - "Node.warn(code, message) form has been deprecated, use Node.warn(warning_instance) instead." -) - -RECORD_XML_PROPERTY = RemovedInPytest4Warning( - 'Fixture renamed from "record_xml_property" to "record_property" as user ' - "properties are now available to all reporters.\n" - '"record_xml_property" is now deprecated.' -) - -COLLECTOR_MAKEITEM = RemovedInPytest4Warning( - "pycollector makeitem was removed as it is an accidentially leaked internal api" -) - -METAFUNC_ADD_CALL = RemovedInPytest4Warning( - "Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.\n" - "Please use Metafunc.parametrize instead." -) - -PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = RemovedInPytest4Warning( - "Defining pytest_plugins in a non-top-level conftest is deprecated, " +PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = ( + "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported " "because it affects the entire directory tree in a non-explicit way.\n" - "Please move it to the top level conftest file instead." + " {}\n" + "Please move it to a top level conftest file at the rootdir:\n" + " {}\n" + "For more information, visit:\n" + " https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" ) -PYTEST_NAMESPACE = RemovedInPytest4Warning( - "pytest_namespace is deprecated and will be removed soon" +PYTEST_CONFIG_GLOBAL = PytestDeprecationWarning( + "the `pytest.config` global is deprecated. Please use `request.config` " + "or `pytest_configure` (if you're a pytest plugin) instead." ) PYTEST_ENSURETEMP = RemovedInPytest4Warning( "pytest/tmpdir_factory.ensuretemp is deprecated, \n" "please use the tmp_path fixture or tmp_path_factory.mktemp" ) + +PYTEST_LOGWARNING = PytestDeprecationWarning( + "pytest_logwarning is deprecated, no longer being called, and will be removed soon\n" + "please use pytest_warning_captured instead" +) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 124b611db..0a1f258e5 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -38,8 +38,6 @@ from _pytest.deprecated import FIXTURE_NAMED_REQUEST from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME -FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}' - @attr.s(frozen=True) class PseudoFixtureDef(object): @@ -469,43 +467,6 @@ class FixtureRequest(FuncargnamesCompatAttr): if argname not in item.funcargs: item.funcargs[argname] = self.getfixturevalue(argname) - def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): - """ (deprecated) Return a testing resource managed by ``setup`` & - ``teardown`` calls. ``scope`` and ``extrakey`` determine when the - ``teardown`` function will be called so that subsequent calls to - ``setup`` would recreate the resource. With pytest-2.3 you often - do not need ``cached_setup()`` as you can directly declare a scope - on a fixture function and register a finalizer through - ``request.addfinalizer()``. - - :arg teardown: function receiving a previously setup resource. - :arg setup: a no-argument function creating a resource. - :arg scope: a string value out of ``function``, ``class``, ``module`` - or ``session`` indicating the caching lifecycle of the resource. - :arg extrakey: added to internal caching key of (funcargname, scope). - """ - from _pytest.deprecated import CACHED_SETUP - - warnings.warn(CACHED_SETUP, stacklevel=2) - if not hasattr(self.config, "_setupcache"): - self.config._setupcache = {} # XXX weakref? - cachekey = (self.fixturename, self._getscopeitem(scope), extrakey) - cache = self.config._setupcache - try: - val = cache[cachekey] - except KeyError: - self._check_scope(self.fixturename, self.scope, scope) - val = setup() - cache[cachekey] = val - if teardown is not None: - - def finalizer(): - del cache[cachekey] - teardown(val) - - self._addfinalizer(finalizer, scope=scope) - return val - def getfixturevalue(self, argname): """ Dynamically run a named fixture function. @@ -605,8 +566,7 @@ class FixtureRequest(FuncargnamesCompatAttr): ) fail(msg, pytrace=False) else: - # indices might not be set if old-style metafunc.addcall() was used - param_index = funcitem.callspec.indices.get(argname, 0) + param_index = funcitem.callspec.indices[argname] # if a parametrize invocation set a scope it will override # the static scope defined with the fixture function paramscopenum = funcitem.callspec._arg2scopenum.get(argname) @@ -982,34 +942,17 @@ def _ensure_immutable_ids(ids): return tuple(ids) -def wrap_function_to_warning_if_called_directly(function, fixture_marker): - """Wrap the given fixture function so we can issue warnings about it being called directly, instead of - used as an argument in a test function. +def wrap_function_to_error_out_if_called_directly(function, fixture_marker): + """Wrap the given fixture function so we can raise an error about it being called directly, + instead of used as an argument in a test function. """ - is_yield_function = is_generator(function) - warning = FIXTURE_FUNCTION_CALL.format( + message = FIXTURE_FUNCTION_CALL.format( name=fixture_marker.name or function.__name__ ) - if is_yield_function: - - @functools.wraps(function) - def result(*args, **kwargs): - __tracebackhide__ = True - warnings.warn(warning, stacklevel=3) - for x in function(*args, **kwargs): - yield x - - else: - - @functools.wraps(function) - def result(*args, **kwargs): - __tracebackhide__ = True - warnings.warn(warning, stacklevel=3) - return function(*args, **kwargs) - - if six.PY2: - result.__wrapped__ = function + @six.wraps(function) + def result(*args, **kwargs): + fail(message, pytrace=False) # keep reference to the original function in our own custom attribute so we don't unwrap # further than this point and lose useful wrappings like @mock.patch (#3774) @@ -1035,7 +978,7 @@ class FixtureFunctionMarker(object): "fixture is being applied more than once to the same function" ) - function = wrap_function_to_warning_if_called_directly(function, self) + function = wrap_function_to_error_out_if_called_directly(function, self) name = self.name or function.__name__ if name == "request": @@ -1155,7 +1098,6 @@ class FixtureManager(object): by a lookup of their FuncFixtureInfo. """ - _argprefix = "pytest_funcarg__" FixtureLookupError = FixtureLookupError FixtureLookupErrorRepr = FixtureLookupErrorRepr @@ -1265,19 +1207,20 @@ class FixtureManager(object): if faclist: fixturedef = faclist[-1] if fixturedef.params is not None: - parametrize_func = getattr(metafunc.function, "parametrize", None) - if parametrize_func is not None: - parametrize_func = parametrize_func.combined - func_params = getattr(parametrize_func, "args", [[None]]) - func_kwargs = getattr(parametrize_func, "kwargs", {}) - # skip directly parametrized arguments - if "argnames" in func_kwargs: - argnames = parametrize_func.kwargs["argnames"] + markers = list(metafunc.definition.iter_markers("parametrize")) + for parametrize_mark in markers: + if "argnames" in parametrize_mark.kwargs: + argnames = parametrize_mark.kwargs["argnames"] + else: + argnames = parametrize_mark.args[0] + + if not isinstance(argnames, (tuple, list)): + argnames = [ + x.strip() for x in argnames.split(",") if x.strip() + ] + if argname in argnames: + break else: - argnames = func_params[0] - if not isinstance(argnames, (tuple, list)): - argnames = [x.strip() for x in argnames.split(",") if x.strip()] - if argname not in func_params and argname not in argnames: metafunc.parametrize( argname, fixturedef.params, @@ -1293,8 +1236,6 @@ class FixtureManager(object): items[:] = reorder_items(items) def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): - from _pytest import deprecated - if nodeid is not NOTSET: holderobj = node_or_obj else: @@ -1303,44 +1244,20 @@ class FixtureManager(object): if holderobj in self._holderobjseen: return - from _pytest.nodes import _CompatProperty - self._holderobjseen.add(holderobj) autousenames = [] for name in dir(holderobj): # The attribute can be an arbitrary descriptor, so the attribute # access below can raise. safe_getatt() ignores such exceptions. - maybe_property = safe_getattr(type(holderobj), name, None) - if isinstance(maybe_property, _CompatProperty): - # deprecated - continue obj = safe_getattr(holderobj, name, None) marker = getfixturemarker(obj) - # fixture functions have a pytest_funcarg__ prefix (pre-2.3 style) - # or are "@pytest.fixture" marked - if marker is None: - if not name.startswith(self._argprefix): - continue - if not callable(obj): - continue - marker = defaultfuncargprefixmarker - - filename, lineno = getfslineno(obj) - warnings.warn_explicit( - deprecated.FUNCARG_PREFIX.format(name=name), - category=None, - filename=str(filename), - lineno=lineno + 1, - ) - name = name[len(self._argprefix) :] - elif not isinstance(marker, FixtureFunctionMarker): + if not isinstance(marker, FixtureFunctionMarker): # magic globals with __getattr__ might have got us a wrong # fixture attribute continue - else: - if marker.name: - name = marker.name - assert not name.startswith(self._argprefix), FIXTURE_MSG.format(name) + + if marker.name: + name = marker.name # during fixture definition we wrap the original fixture function # to issue a warning if called directly, so here we unwrap it in order to not emit the warning diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 625f59e5a..2dfbfd0c9 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,7 +1,7 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ from pluggy import HookspecMarker -from .deprecated import PYTEST_NAMESPACE +from _pytest.deprecated import PYTEST_LOGWARNING hookspec = HookspecMarker("pytest") @@ -24,32 +24,6 @@ def pytest_addhooks(pluginmanager): """ -@hookspec(historic=True, warn_on_impl=PYTEST_NAMESPACE) -def pytest_namespace(): - """ - return dict of name->object to be made globally available in - the pytest namespace. - - This hook is called at plugin registration time. - - .. note:: - This hook is incompatible with ``hookwrapper=True``. - - .. warning:: - This hook has been **deprecated** and will be removed in pytest 4.0. - - Plugins whose users depend on the current namespace functionality should prepare to migrate to a - namespace they actually own. - - To support the migration it's suggested to trigger ``DeprecationWarnings`` for objects they put into the - pytest namespace. - - A stopgap measure to avoid the warning is to monkeypatch the ``pytest`` module, but just as the - ``pytest_namespace`` hook this should be seen as a temporary measure to be removed in future versions after - an appropriate transition period. - """ - - @hookspec(historic=True) def pytest_plugin_registered(plugin, manager): """ a new pytest plugin got registered. @@ -524,7 +498,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus): """ -@hookspec(historic=True) +@hookspec(historic=True, warn_on_impl=PYTEST_LOGWARNING) def pytest_logwarning(message, code, nodeid, fslocation): """ .. deprecated:: 3.8 diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 09847c942..1a06ea6d1 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -263,16 +263,6 @@ def record_property(request): return append_property -@pytest.fixture -def record_xml_property(record_property, request): - """(Deprecated) use record_property.""" - from _pytest import deprecated - - request.node.warn(deprecated.RECORD_XML_PROPERTY) - - return record_property - - @pytest.fixture def record_xml_attribute(request): """Add extra xml attributes to the tag for the calling test. @@ -323,6 +313,11 @@ def pytest_addoption(parser): "one of no|system-out|system-err", default="no", ) # choices=['no', 'stdout', 'stderr']) + parser.addini( + "junit_duration_report", + "Duration time to report: one of total|call", + default="total", + ) # choices=['total', 'call']) def pytest_configure(config): @@ -334,6 +329,7 @@ def pytest_configure(config): config.option.junitprefix, config.getini("junit_suite_name"), config.getini("junit_logging"), + config.getini("junit_duration_report"), ) config.pluginmanager.register(config._xml) @@ -361,12 +357,20 @@ def mangle_test_address(address): class LogXML(object): - def __init__(self, logfile, prefix, suite_name="pytest", logging="no"): + def __init__( + self, + logfile, + prefix, + suite_name="pytest", + logging="no", + report_duration="total", + ): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) self.prefix = prefix self.suite_name = suite_name self.logging = logging + self.report_duration = report_duration self.stats = dict.fromkeys(["error", "passed", "failure", "skipped"], 0) self.node_reporters = {} # nodeid -> _NodeReporter self.node_reporters_ordered = [] @@ -500,8 +504,9 @@ class LogXML(object): """accumulates total duration for nodeid from given report and updates the Junit.testcase with the new total if already created. """ - reporter = self.node_reporter(report) - reporter.duration += getattr(report, "duration", 0.0) + if self.report_duration == "total" or report.when == self.report_duration: + reporter = self.node_reporter(report) + reporter.duration += getattr(report, "duration", 0.0) def pytest_collectreport(self, report): if not report.passed: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index df4f1c8fb..d0d826bb6 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -8,6 +8,7 @@ import functools import os import pkgutil import sys +import warnings import attr import py @@ -18,6 +19,7 @@ from _pytest import nodes from _pytest.config import directory_arg from _pytest.config import hookimpl from _pytest.config import UsageError +from _pytest.deprecated import PYTEST_CONFIG_GLOBAL from _pytest.outcomes import exit from _pytest.runner import collect_one_node @@ -167,8 +169,24 @@ def pytest_addoption(parser): ) +class _ConfigDeprecated(object): + def __init__(self, config): + self.__dict__["_config"] = config + + def __getattr__(self, attr): + warnings.warn(PYTEST_CONFIG_GLOBAL, stacklevel=2) + return getattr(self._config, attr) + + def __setattr__(self, attr, val): + warnings.warn(PYTEST_CONFIG_GLOBAL, stacklevel=2) + return setattr(self._config, attr, val) + + def __repr__(self): + return "{}({!r})".format(type(self).__name__, self._config) + + def pytest_configure(config): - __import__("pytest").config = config # compatibility + __import__("pytest").config = _ConfigDeprecated(config) # compatibility def wrap_session(config, doit): @@ -187,8 +205,8 @@ def wrap_session(config, doit): raise except Failed: session.exitstatus = EXIT_TESTSFAILED - except KeyboardInterrupt: - excinfo = _pytest._code.ExceptionInfo() + except (KeyboardInterrupt, exit.Exception): + excinfo = _pytest._code.ExceptionInfo.from_current() exitstatus = EXIT_INTERRUPTED if initstate <= 2 and isinstance(excinfo.value, exit.Exception): sys.stderr.write("{}: {}\n".format(excinfo.typename, excinfo.value.msg)) @@ -197,7 +215,7 @@ def wrap_session(config, doit): config.hook.pytest_keyboard_interrupt(excinfo=excinfo) session.exitstatus = exitstatus except: # noqa - excinfo = _pytest._code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo.from_current() config.notify_exception(excinfo, config.option) session.exitstatus = EXIT_INTERNALERROR if excinfo.errisinstance(SystemExit): diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index b6495dd03..bc4c467f9 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -11,19 +11,10 @@ from .structures import Mark from .structures import MARK_GEN from .structures import MarkDecorator from .structures import MarkGenerator -from .structures import MarkInfo from .structures import ParameterSet -from .structures import transfer_markers from _pytest.config import UsageError -__all__ = [ - "Mark", - "MarkInfo", - "MarkDecorator", - "MarkGenerator", - "transfer_markers", - "get_empty_parameterset_mark", -] +__all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"] def param(*values, **kw): diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index b8fa011d1..49695b56f 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -1,17 +1,15 @@ import inspect import warnings from collections import namedtuple -from functools import reduce from operator import attrgetter import attr -from six.moves import map +import six +from ..compat import ascii_escaped from ..compat import getfslineno from ..compat import MappingMixin from ..compat import NOTSET -from ..deprecated import MARK_INFO_ATTRIBUTE -from ..deprecated import MARK_PARAMETERSET_UNPACKING from _pytest.outcomes import fail @@ -70,46 +68,33 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): else: assert isinstance(marks, (tuple, list, set)) - def param_extract_id(id=None): - return id - - id_ = param_extract_id(**kw) + id_ = kw.pop("id", None) + if id_ is not None: + if not isinstance(id_, six.string_types): + raise TypeError( + "Expected id to be a string, got {}: {!r}".format(type(id_), id_) + ) + id_ = ascii_escaped(id_) return cls(values, marks, id_) @classmethod - def extract_from(cls, parameterset, belonging_definition, legacy_force_tuple=False): + def extract_from(cls, parameterset, force_tuple=False): """ :param parameterset: a legacy style parameterset that may or may not be a tuple, and may or may not be wrapped into a mess of mark objects - :param legacy_force_tuple: + :param force_tuple: enforce tuple wrapping so single argument tuple values don't get decomposed and break tests - - :param belonging_definition: the item that we will be extracting the parameters from. """ if isinstance(parameterset, cls): return parameterset - if not isinstance(parameterset, MarkDecorator) and legacy_force_tuple: + if force_tuple: return cls.param(parameterset) - - newmarks = [] - argval = parameterset - while isinstance(argval, MarkDecorator): - newmarks.append( - MarkDecorator(Mark(argval.markname, argval.args[:-1], argval.kwargs)) - ) - argval = argval.args[-1] - assert not isinstance(argval, ParameterSet) - if legacy_force_tuple: - argval = (argval,) - - if newmarks and belonging_definition is not None: - belonging_definition.warn(MARK_PARAMETERSET_UNPACKING) - - return cls(argval, marks=newmarks, id=None) + else: + return cls(parameterset, marks=[], id=None) @classmethod def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): @@ -119,12 +104,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): else: force_tuple = False parameters = [ - ParameterSet.extract_from( - x, - legacy_force_tuple=force_tuple, - belonging_definition=function_definition, - ) - for x in argvalues + ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues ] del argvalues @@ -132,11 +112,21 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): # check all parameter sets have the correct number of values for param in parameters: if len(param.values) != len(argnames): - raise ValueError( - 'In "parametrize" the number of values ({}) must be ' - "equal to the number of names ({})".format( - param.values, argnames - ) + msg = ( + '{nodeid}: in "parametrize" the number of names ({names_len}):\n' + " {names}\n" + "must be equal to the number of values ({values_len}):\n" + " {values}" + ) + fail( + msg.format( + nodeid=function_definition.nodeid, + values=param.values, + names=argnames, + names_len=len(argnames), + values_len=len(param.values), + ), + pytrace=False, ) else: # empty parameter set (likely computed at runtime): create a single @@ -240,11 +230,7 @@ class MarkDecorator(object): func = args[0] is_class = inspect.isclass(func) if len(args) == 1 and (istestfunc(func) or is_class): - if is_class: - store_mark(func, self.mark) - else: - store_legacy_markinfo(func, self.mark) - store_mark(func, self.mark) + store_mark(func, self.mark) return func return self.with_args(*args, **kwargs) @@ -266,7 +252,13 @@ def normalize_mark_list(mark_list): :type mark_list: List[Union[Mark, Markdecorator]] :rtype: List[Mark] """ - return [getattr(mark, "mark", mark) for mark in mark_list] # unpack MarkDecorator + extracted = [ + getattr(mark, "mark", mark) for mark in mark_list + ] # unpack MarkDecorator + for mark in extracted: + if not isinstance(mark, Mark): + raise TypeError("got {!r} instead of Mark".format(mark)) + return [x for x in extracted if isinstance(x, Mark)] def store_mark(obj, mark): @@ -279,90 +271,6 @@ def store_mark(obj, mark): obj.pytestmark = get_unpacked_marks(obj) + [mark] -def store_legacy_markinfo(func, mark): - """create the legacy MarkInfo objects and put them onto the function - """ - if not isinstance(mark, Mark): - raise TypeError("got {mark!r} instead of a Mark".format(mark=mark)) - holder = getattr(func, mark.name, None) - if holder is None: - holder = MarkInfo.for_mark(mark) - setattr(func, mark.name, holder) - elif isinstance(holder, MarkInfo): - holder.add_mark(mark) - - -def transfer_markers(funcobj, cls, mod): - """ - this function transfers class level markers and module level markers - into function level markinfo objects - - this is the main reason why marks are so broken - the resolution will involve phasing out function level MarkInfo objects - - """ - for obj in (cls, mod): - for mark in get_unpacked_marks(obj): - if not _marked(funcobj, mark): - store_legacy_markinfo(funcobj, mark) - - -def _marked(func, mark): - """ Returns True if :func: is already marked with :mark:, False otherwise. - This can happen if marker is applied to class and the test file is - invoked more than once. - """ - try: - func_mark = getattr(func, getattr(mark, "combined", mark).name) - except AttributeError: - return False - return any(mark == info.combined for info in func_mark) - - -@attr.s(repr=False) -class MarkInfo(object): - """ Marking object created by :class:`MarkDecorator` instances. """ - - _marks = attr.ib(converter=list) - - @_marks.validator - def validate_marks(self, attribute, value): - for item in value: - if not isinstance(item, Mark): - raise ValueError( - "MarkInfo expects Mark instances, got {!r} ({!r})".format( - item, type(item) - ) - ) - - combined = attr.ib( - repr=False, - default=attr.Factory( - lambda self: reduce(Mark.combined_with, self._marks), takes_self=True - ), - ) - - name = alias("combined.name", warning=MARK_INFO_ATTRIBUTE) - args = alias("combined.args", warning=MARK_INFO_ATTRIBUTE) - kwargs = alias("combined.kwargs", warning=MARK_INFO_ATTRIBUTE) - - @classmethod - def for_mark(cls, mark): - return cls([mark]) - - def __repr__(self): - return "".format(self.combined) - - def add_mark(self, mark): - """ add a MarkInfo with the given args and kwargs. """ - self._marks.append(mark) - self.combined = self.combined.combined_with(mark) - - def __iter__(self): - """ yield MarkInfo objects each relating to a marking-call. """ - return map(MarkInfo.for_mark, self._marks) - - class MarkGenerator(object): """ Factory for :class:`MarkDecorator` objects - exposed as a ``pytest.mark`` singleton instance. Example:: diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 86e541152..00ec80894 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -5,13 +5,11 @@ from __future__ import print_function import os import warnings -import attr import py import six import _pytest._code from _pytest.compat import getfslineno -from _pytest.mark.structures import MarkInfo from _pytest.mark.structures import NodeKeywords from _pytest.outcomes import fail @@ -56,22 +54,6 @@ def ischildnode(baseid, nodeid): return node_parts[: len(base_parts)] == base_parts -@attr.s -class _CompatProperty(object): - name = attr.ib() - - def __get__(self, obj, owner): - if obj is None: - return self - - from _pytest.deprecated import COMPAT_PROPERTY - - warnings.warn( - COMPAT_PROPERTY.format(name=self.name, owner=owner.__name__), stacklevel=2 - ) - return getattr(__import__("pytest"), self.name) - - class Node(object): """ base class for Collector and Item the test collection tree. Collector subclasses have children, Items are terminal nodes.""" @@ -119,95 +101,10 @@ class Node(object): """ fspath sensitive hook proxy used to call pytest hooks""" return self.session.gethookproxy(self.fspath) - Module = _CompatProperty("Module") - Class = _CompatProperty("Class") - Instance = _CompatProperty("Instance") - Function = _CompatProperty("Function") - File = _CompatProperty("File") - Item = _CompatProperty("Item") - - def _getcustomclass(self, name): - maybe_compatprop = getattr(type(self), name) - if isinstance(maybe_compatprop, _CompatProperty): - return getattr(__import__("pytest"), name) - else: - from _pytest.deprecated import CUSTOM_CLASS - - cls = getattr(self, name) - self.warn(CUSTOM_CLASS.format(name=name, type_name=type(self).__name__)) - return cls - def __repr__(self): - return "<%s %r>" % (self.__class__.__name__, getattr(self, "name", None)) + return "<%s %s>" % (self.__class__.__name__, getattr(self, "name", None)) - def warn(self, _code_or_warning=None, message=None, code=None): - """Issue a warning for this item. - - Warnings will be displayed after the test session, unless explicitly suppressed. - - This can be called in two forms: - - **Warning instance** - - This was introduced in pytest 3.8 and uses the standard warning mechanism to issue warnings. - - .. code-block:: python - - node.warn(PytestWarning("some message")) - - The warning instance must be a subclass of :class:`pytest.PytestWarning`. - - **code/message (deprecated)** - - This form was used in pytest prior to 3.8 and is considered deprecated. Using this form will emit another - warning about the deprecation: - - .. code-block:: python - - node.warn("CI", "some message") - - :param Union[Warning,str] _code_or_warning: - warning instance or warning code (legacy). This parameter receives an underscore for backward - compatibility with the legacy code/message form, and will be replaced for something - more usual when the legacy form is removed. - - :param Union[str,None] message: message to display when called in the legacy form. - :param str code: code for the warning, in legacy form when using keyword arguments. - :return: - """ - if message is None: - if _code_or_warning is None: - raise ValueError("code_or_warning must be given") - self._std_warn(_code_or_warning) - else: - if _code_or_warning and code: - raise ValueError( - "code_or_warning and code cannot both be passed to this function" - ) - code = _code_or_warning or code - self._legacy_warn(code, message) - - def _legacy_warn(self, code, message): - """ - .. deprecated:: 3.8 - - Use :meth:`Node.std_warn <_pytest.nodes.Node.std_warn>` instead. - - Generate a warning with the given code and message for this item. - """ - from _pytest.deprecated import NODE_WARN - - self._std_warn(NODE_WARN) - - assert isinstance(code, str) - fslocation = get_fslocation_from_item(self) - self.ihook.pytest_logwarning.call_historic( - kwargs=dict( - code=code, message=message, nodeid=self.nodeid, fslocation=fslocation - ) - ) - - def _std_warn(self, warning): + def warn(self, warning): """Issue a warning for this item. Warnings will be displayed after the test session, unless explicitly suppressed @@ -215,6 +112,12 @@ class Node(object): :param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning. :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning. + + Example usage:: + + .. code-block:: python + + node.warn(PytestWarning("some message")) """ from _pytest.warning_types import PytestWarning @@ -307,20 +210,6 @@ class Node(object): """ return next(self.iter_markers(name=name), default) - def get_marker(self, name): - """ get a marker object from this node or None if - the node doesn't have a marker with that name. - - .. deprecated:: 3.6 - This function has been deprecated in favor of - :meth:`Node.get_closest_marker <_pytest.nodes.Node.get_closest_marker>` and - :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`, see :ref:`update marker code` - for more details. - """ - markers = list(self.iter_markers(name=name)) - if markers: - return MarkInfo(markers) - def listextrakeywords(self): """ Return a set of all extra keywords in self and any parents.""" extra_keywords = set() diff --git a/src/_pytest/nose.py b/src/_pytest/nose.py index 4bfa9c583..6facc547f 100644 --- a/src/_pytest/nose.py +++ b/src/_pytest/nose.py @@ -23,20 +23,15 @@ def get_skip_exceptions(): def pytest_runtest_makereport(item, call): if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()): # let's substitute the excinfo with a pytest.skip one - call2 = call.__class__(lambda: runner.skip(str(call.excinfo.value)), call.when) + call2 = runner.CallInfo.from_call( + lambda: runner.skip(str(call.excinfo.value)), call.when + ) call.excinfo = call2.excinfo @hookimpl(trylast=True) def pytest_runtest_setup(item): if is_potential_nosetest(item): - if isinstance(item.parent, python.Generator): - gen = item.parent - if not hasattr(gen, "_nosegensetup"): - call_optional(gen.obj, "setup") - if isinstance(gen.parent, python.Instance): - call_optional(gen.parent.obj, "setup") - gen._nosegensetup = True if not call_optional(item.obj, "setup"): # call module level setup if there is no object level one call_optional(item.parent.obj, "setup") @@ -53,11 +48,6 @@ def teardown_nose(item): # del item.parent._nosegensetup -def pytest_make_collect_report(collector): - if isinstance(collector, python.Generator): - call_optional(collector.obj, "setup") - - def is_potential_nosetest(item): # extra check needed since we do not do nose style setup/teardown # on direct unittest style classes diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index cd08c0d48..d27939e30 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -49,13 +49,13 @@ class Failed(OutcomeException): __module__ = "builtins" -class Exit(KeyboardInterrupt): +class Exit(SystemExit): """ raised for immediate program exits (no tracebacks/summaries)""" def __init__(self, msg="unknown reason", returncode=None): self.msg = msg self.returncode = returncode - KeyboardInterrupt.__init__(self, msg) + SystemExit.__init__(self, msg) # exposed helper methods @@ -63,7 +63,7 @@ class Exit(KeyboardInterrupt): def exit(msg, returncode=None): """ - Exit testing process as if KeyboardInterrupt was triggered. + Exit testing process as if SystemExit was triggered. :param str msg: message to display upon exit. :param int returncode: return code to be used when exiting pytest. @@ -137,10 +137,15 @@ def xfail(reason=""): xfail.Exception = XFailed -def importorskip(modname, minversion=None): - """ return imported module if it has at least "minversion" as its - __version__ attribute. If no minversion is specified the a skip - is only triggered if the module can not be imported. +def importorskip(modname, minversion=None, reason=None): + """Imports and returns the requested module ``modname``, or skip the current test + if the module cannot be imported. + + :param str modname: the name of the module to import + :param str minversion: if given, the imported module ``__version__`` attribute must be + at least this minimal version, otherwise the test is still skipped. + :param str reason: if given, this reason is shown as the message when the module + cannot be imported. """ import warnings @@ -159,7 +164,9 @@ def importorskip(modname, minversion=None): # Do not raise chained exception here(#1485) should_skip = True if should_skip: - raise Skipped("could not import %r" % (modname,), allow_module_level=True) + if reason is None: + reason = "could not import %r" % (modname,) + raise Skipped(reason, allow_module_level=True) mod = sys.modules[modname] if minversion is None: return mod diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 8912ca060..48a50178f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -38,13 +38,12 @@ from _pytest.compat import safe_str from _pytest.compat import STRING_TYPES from _pytest.config import hookimpl from _pytest.main import FSHookProxy +from _pytest.mark import MARK_GEN from _pytest.mark.structures import get_unpacked_marks from _pytest.mark.structures import normalize_mark_list -from _pytest.mark.structures import transfer_markers from _pytest.outcomes import fail from _pytest.pathlib import parts from _pytest.warning_types import PytestWarning -from _pytest.warning_types import RemovedInPytest4Warning def pyobj_property(name): @@ -125,10 +124,10 @@ def pytest_generate_tests(metafunc): # those alternative spellings are common - raise a specific error to alert # the user alt_spellings = ["parameterize", "parametrise", "parameterise"] - for attr in alt_spellings: - if hasattr(metafunc.function, attr): + for mark_name in alt_spellings: + if metafunc.definition.get_closest_marker(mark_name): msg = "{0} has '{1}' mark, spelling should be 'parametrize'" - fail(msg.format(metafunc.function.__name__, attr), pytrace=False) + fail(msg.format(metafunc.function.__name__, mark_name), pytrace=False) for marker in metafunc.definition.iter_markers(name="parametrize"): metafunc.parametrize(*marker.args, **marker.kwargs) @@ -199,7 +198,6 @@ def pytest_pycollect_makeitem(collector, name, obj): # nothing was collected elsewhere, let's do it here if safe_isclass(obj): if collector.istestclass(obj, name): - Class = collector._getcustomclass("Class") outcome.force_result(Class(name, parent=collector)) elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it @@ -219,7 +217,10 @@ def pytest_pycollect_makeitem(collector, name, obj): ) elif getattr(obj, "__test__", True): if is_generator(obj): - res = Generator(name, parent=collector) + res = Function(name, parent=collector) + reason = deprecated.YIELD_TESTS.format(name=name) + res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) + res.warn(PytestWarning(reason)) else: res = list(collector._genfunctions(name, obj)) outcome.force_result(res) @@ -375,10 +376,6 @@ class PyCollector(PyobjMixin, nodes.Collector): values.sort(key=lambda item: item.reportinfo()[:2]) return values - def makeitem(self, name, obj): - warnings.warn(deprecated.COLLECTOR_MAKEITEM, stacklevel=2) - self._makeitem(name, obj) - def _makeitem(self, name, obj): # assert self.ihook.fspath == self.fspath, self return self.ihook.pytest_pycollect_makeitem(collector=self, name=name, obj=obj) @@ -387,7 +384,6 @@ class PyCollector(PyobjMixin, nodes.Collector): module = self.getparent(Module).obj clscol = self.getparent(Class) cls = clscol and clscol.obj or None - transfer_markers(funcobj, cls, module) fm = self.session._fixturemanager definition = FunctionDefinition(name=name, parent=self, callobj=funcobj) @@ -408,7 +404,6 @@ class PyCollector(PyobjMixin, nodes.Collector): else: self.ihook.pytest_generate_tests(metafunc=metafunc) - Function = self._getcustomclass("Function") if not metafunc._calls: yield Function(name, parent=self, fixtureinfo=fixtureinfo) else: @@ -450,7 +445,7 @@ class Module(nodes.File, PyCollector): mod = self.fspath.pyimport(ensuresyspath=importmode) except SyntaxError: raise self.CollectError( - _pytest._code.ExceptionInfo().getrepr(style="short") + _pytest._code.ExceptionInfo.from_current().getrepr(style="short") ) except self.fspath.ImportMismatchError: e = sys.exc_info()[1] @@ -466,7 +461,7 @@ class Module(nodes.File, PyCollector): except ImportError: from _pytest._code.code import ExceptionInfo - exc_info = ExceptionInfo() + exc_info = ExceptionInfo.from_current() if self.config.getoption("verbose") < 2: exc_info.traceback = exc_info.traceback.filter(filter_traceback) exc_repr = ( @@ -648,7 +643,7 @@ class Class(PyCollector): ) ) return [] - return [self._getcustomclass("Instance")(name="()", parent=self)] + return [Instance(name="()", parent=self)] def setup(self): setup_class = _get_xunit_func(self.obj, "setup_class") @@ -739,51 +734,6 @@ class FunctionMixin(PyobjMixin): return self._repr_failure_py(excinfo, style=style) -class Generator(FunctionMixin, PyCollector): - def collect(self): - - # test generators are seen as collectors but they also - # invoke setup/teardown on popular request - # (induced by the common "test_*" naming shared with normal tests) - from _pytest import deprecated - - self.warn(deprecated.YIELD_TESTS) - - self.session._setupstate.prepare(self) - # see FunctionMixin.setup and test_setupstate_is_preserved_134 - self._preservedparent = self.parent.obj - values = [] - seen = {} - _Function = self._getcustomclass("Function") - for i, x in enumerate(self.obj()): - name, call, args = self.getcallargs(x) - if not callable(call): - raise TypeError("%r yielded non callable test %r" % (self.obj, call)) - if name is None: - name = "[%d]" % i - else: - name = "['%s']" % name - if name in seen: - raise ValueError( - "%r generated tests with non-unique name %r" % (self, name) - ) - seen[name] = True - values.append(_Function(name, self, args=args, callobj=call)) - return values - - def getcallargs(self, obj): - if not isinstance(obj, (tuple, list)): - obj = (obj,) - # explicit naming - if isinstance(obj[0], six.string_types): - name = obj[0] - obj = obj[1:] - else: - name = None - call, args = obj[0], obj[1:] - return name, call, args - - def hasinit(obj): init = getattr(obj, "__init__", None) if init: @@ -1065,48 +1015,6 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): pytrace=False, ) - def addcall(self, funcargs=None, id=NOTSET, param=NOTSET): - """ Add a new call to the underlying test function during the collection phase of a test run. - - .. deprecated:: 3.3 - - Use :meth:`parametrize` instead. - - Note that request.addcall() is called during the test collection phase prior and - independently to actual test execution. You should only use addcall() - if you need to specify multiple arguments of a test function. - - :arg funcargs: argument keyword dictionary used when invoking - the test function. - - :arg id: used for reporting and identification purposes. If you - don't supply an `id` an automatic unique id will be generated. - - :arg param: a parameter which will be exposed to a later fixture function - invocation through the ``request.param`` attribute. - """ - warnings.warn(deprecated.METAFUNC_ADD_CALL, stacklevel=2) - - assert funcargs is None or isinstance(funcargs, dict) - if funcargs is not None: - for name in funcargs: - if name not in self.fixturenames: - fail("funcarg %r not used in this function." % name) - else: - funcargs = {} - if id is None: - raise ValueError("id=None not allowed") - if id is NOTSET: - id = len(self._calls) - id = str(id) - if id in self._ids: - raise ValueError("duplicate id %r" % id) - self._ids.add(id) - - cs = CallSpec2(self) - cs.setall(funcargs, id, param) - self._calls.append(cs) - def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): """Find the most appropriate scope for a parametrized call based on its arguments. @@ -1148,13 +1056,11 @@ def _idval(val, argname, idx, idfn, item, config): s = idfn(val) except Exception as e: # See issue https://github.com/pytest-dev/pytest/issues/2169 - msg = ( - "While trying to determine id of parameter {} at position " - "{} the following exception was raised:\n".format(argname, idx) - ) + msg = "{}: error raised while trying to determine id of parameter '{}' at position {}\n" + msg = msg.format(item.nodeid, argname, idx) + # we only append the exception type and message because on Python 2 reraise does nothing msg += " {}: {}\n".format(type(e).__name__, e) - msg += "This warning will be an error error in pytest-4.0." - item.warn(RemovedInPytest4Warning(msg)) + six.raise_from(ValueError(msg), e) if s: return ascii_escaped(s) @@ -1326,8 +1232,7 @@ def _showfixtures_main(config, session): tw.line(" %s: no docstring available" % (loc,), red=True) -def write_docstring(tw, doc): - INDENT = " " +def write_docstring(tw, doc, indent=" "): doc = doc.rstrip() if "\n" in doc: firstline, rest = doc.split("\n", 1) @@ -1335,11 +1240,11 @@ def write_docstring(tw, doc): firstline, rest = doc, "" if firstline.strip(): - tw.line(INDENT + firstline.strip()) + tw.line(indent + firstline.strip()) if rest: for line in dedent(rest).split("\n"): - tw.write(INDENT + line + "\n") + tw.write(indent + line + "\n") class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): @@ -1384,6 +1289,20 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): if keywords: self.keywords.update(keywords) + # todo: this is a hell of a hack + # https://github.com/pytest-dev/pytest/issues/4569 + + self.keywords.update( + dict.fromkeys( + [ + mark.name + for mark in self.iter_markers() + if mark.name not in self.keywords + ], + True, + ) + ) + if fixtureinfo is None: fixtureinfo = self.session._fixturemanager.getfixtureinfo( self, self.obj, self.cls, funcargs=not self._isyieldedfunction() diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 37eafa7f0..4e4740192 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,6 +1,9 @@ +from __future__ import absolute_import + import math import pprint import sys +import warnings from decimal import Decimal from numbers import Number @@ -10,9 +13,11 @@ from six.moves import filterfalse from six.moves import zip import _pytest._code +from _pytest import deprecated from _pytest.compat import isclass +from _pytest.compat import Iterable from _pytest.compat import Mapping -from _pytest.compat import Sequence +from _pytest.compat import Sized from _pytest.compat import STRING_TYPES from _pytest.outcomes import fail @@ -182,7 +187,7 @@ class ApproxMapping(ApproxBase): raise _non_numeric_type_error(self.expected, at="key={!r}".format(key)) -class ApproxSequence(ApproxBase): +class ApproxSequencelike(ApproxBase): """ Perform approximate comparisons where the expected value is a sequence of numbers. @@ -518,10 +523,14 @@ def approx(expected, rel=None, abs=None, nan_ok=False): cls = ApproxScalar elif isinstance(expected, Mapping): cls = ApproxMapping - elif isinstance(expected, Sequence) and not isinstance(expected, STRING_TYPES): - cls = ApproxSequence elif _is_numpy_array(expected): cls = ApproxNumpy + elif ( + isinstance(expected, Iterable) + and isinstance(expected, Sized) + and not isinstance(expected, STRING_TYPES) + ): + cls = ApproxSequencelike else: raise _non_numeric_type_error(expected, at=None) @@ -547,29 +556,47 @@ def _is_numpy_array(obj): def raises(expected_exception, *args, **kwargs): r""" Assert that a code block/function call raises ``expected_exception`` - and raise a failure exception otherwise. + or raise a failure exception otherwise. - :arg message: if specified, provides a custom failure message if the - exception is not raised - :arg match: if specified, asserts that the exception matches a text or regex + :kwparam match: if specified, asserts that the exception matches a text or regex - This helper produces a ``ExceptionInfo()`` object (see below). + :kwparam message: **(deprecated since 4.1)** if specified, provides a custom failure message + if the exception is not raised - You may use this function as a context manager:: + .. currentmodule:: _pytest._code + + Use ``pytest.raises`` as a context manager, which will capture the exception of the given + type:: >>> with raises(ZeroDivisionError): ... 1/0 - .. versionchanged:: 2.10 + If the code block does not raise the expected exception (``ZeroDivisionError`` in the example + above), or no exception at all, the check will fail instead. - In the context manager form you may use the keyword argument - ``message`` to specify a custom failure message:: + You can also use the keyword argument ``match`` to assert that the + exception matches a text or regex:: - >>> with raises(ZeroDivisionError, message="Expecting ZeroDivisionError"): - ... pass - Traceback (most recent call last): - ... - Failed: Expecting ZeroDivisionError + >>> with raises(ValueError, match='must be 0 or None'): + ... raise ValueError("value must be 0 or None") + + >>> with raises(ValueError, match=r'must be \d+$'): + ... raise ValueError("value must be 42") + + The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the + details of the captured exception:: + + >>> with raises(ValueError) as exc_info: + ... raise ValueError("value must be 42") + >>> assert exc_info.type is ValueError + >>> assert exc_info.value.args[0] == "value must be 42" + + .. deprecated:: 4.1 + + In the context manager form you may use the keyword argument + ``message`` to specify a custom failure message that will be displayed + in case the ``pytest.raises`` check fails. This has been deprecated as it + is considered error prone as users often mean to use ``match`` instead. .. note:: @@ -583,7 +610,7 @@ def raises(expected_exception, *args, **kwargs): >>> with raises(ValueError) as exc_info: ... if value > 10: ... raise ValueError("value must be <= 10") - ... assert exc_info.type == ValueError # this will not execute + ... assert exc_info.type is ValueError # this will not execute Instead, the following approach must be taken (note the difference in scope):: @@ -592,22 +619,9 @@ def raises(expected_exception, *args, **kwargs): ... if value > 10: ... raise ValueError("value must be <= 10") ... - >>> assert exc_info.type == ValueError + >>> assert exc_info.type is ValueError - - Since version ``3.1`` you can use the keyword argument ``match`` to assert that the - exception matches a text or regex:: - - >>> with raises(ValueError, match='must be 0 or None'): - ... raise ValueError("value must be 0 or None") - - >>> with raises(ValueError, match=r'must be \d+$'): - ... raise ValueError("value must be 42") - - **Legacy forms** - - The forms below are fully supported but are discouraged for new code because the - context manager form is regarded as more readable and less error-prone. + **Legacy form** It is possible to specify a callable by passing a to-be-called lambda:: @@ -623,17 +637,8 @@ def raises(expected_exception, *args, **kwargs): >>> raises(ZeroDivisionError, f, x=0) - It is also possible to pass a string to be evaluated at runtime:: - - >>> raises(ZeroDivisionError, "f(0)") - - - The string will be evaluated using the same ``locals()`` and ``globals()`` - at the moment of the ``raises`` call. - - .. currentmodule:: _pytest._code - - Consult the API of ``excinfo`` objects: :class:`ExceptionInfo`. + The form above is fully supported but discouraged for new code because the + context manager form is regarded as more readable and less error-prone. .. note:: Similar to caught exception objects in Python, explicitly clearing @@ -664,6 +669,7 @@ def raises(expected_exception, *args, **kwargs): if not args: if "message" in kwargs: message = kwargs.pop("message") + warnings.warn(deprecated.RAISES_MESSAGE_PARAMETER, stacklevel=2) if "match" in kwargs: match_expr = kwargs.pop("match") if kwargs: @@ -672,6 +678,7 @@ def raises(expected_exception, *args, **kwargs): raise TypeError(msg) return RaisesContext(expected_exception, message, match_expr) elif isinstance(args[0], str): + warnings.warn(deprecated.RAISES_EXEC, stacklevel=2) code, = args assert isinstance(code, str) frame = sys._getframe(1) @@ -684,13 +691,13 @@ def raises(expected_exception, *args, **kwargs): # XXX didn't mean f_globals == f_locals something special? # this is destroyed here ... except expected_exception: - return _pytest._code.ExceptionInfo() + return _pytest._code.ExceptionInfo.from_current() else: func = args[0] try: func(*args[1:], **kwargs) except expected_exception: - return _pytest._code.ExceptionInfo() + return _pytest._code.ExceptionInfo.from_current() fail(message) @@ -705,7 +712,7 @@ class RaisesContext(object): self.excinfo = None def __enter__(self): - self.excinfo = object.__new__(_pytest._code.ExceptionInfo) + self.excinfo = _pytest._code.ExceptionInfo.for_later() return self.excinfo def __exit__(self, *tp): diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 4f3ab7f29..f39f7aee7 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -11,6 +11,7 @@ import warnings import six import _pytest._code +from _pytest.deprecated import WARNS_EXEC from _pytest.fixtures import yield_fixture from _pytest.outcomes import fail @@ -89,6 +90,7 @@ def warns(expected_warning, *args, **kwargs): match_expr = kwargs.pop("match") return WarningsChecker(expected_warning, match_expr=match_expr) elif isinstance(args[0], str): + warnings.warn(WARNS_EXEC, stacklevel=2) code, = args assert isinstance(code, str) frame = sys._getframe(1) diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index ab2d0f98b..bdf8130fd 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -34,9 +34,9 @@ def pytest_configure(config): config.pluginmanager.register(config._resultlog) from _pytest.deprecated import RESULT_LOG - from _pytest.warnings import _issue_config_warning + from _pytest.warnings import _issue_warning_captured - _issue_config_warning(RESULT_LOG, config, stacklevel=2) + _issue_warning_captured(RESULT_LOG, config.hook, stacklevel=2) def pytest_unconfigure(config): diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 86298a7aa..538e13403 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -8,12 +8,14 @@ import os import sys from time import time +import attr import six from .reports import CollectErrorRepr from .reports import CollectReport from .reports import TestReport from _pytest._code.code import ExceptionInfo +from _pytest.outcomes import Exit from _pytest.outcomes import skip from _pytest.outcomes import Skipped from _pytest.outcomes import TEST_OUTCOME @@ -189,43 +191,58 @@ def check_interactive_exception(call, report): def call_runtest_hook(item, when, **kwds): hookname = "pytest_runtest_" + when ihook = getattr(item.ihook, hookname) - return CallInfo( - lambda: ihook(item=item, **kwds), - when=when, - treat_keyboard_interrupt_as_exception=item.config.getvalue("usepdb"), + reraise = (Exit,) + if not item.config.getvalue("usepdb"): + reraise += (KeyboardInterrupt,) + return CallInfo.from_call( + lambda: ihook(item=item, **kwds), when=when, reraise=reraise ) +@attr.s(repr=False) class CallInfo(object): """ Result/Exception info a function invocation. """ - #: None or ExceptionInfo object. - excinfo = None + _result = attr.ib() + # type: Optional[ExceptionInfo] + excinfo = attr.ib() + start = attr.ib() + stop = attr.ib() + when = attr.ib() - def __init__(self, func, when, treat_keyboard_interrupt_as_exception=False): + @property + def result(self): + if self.excinfo is not None: + raise AttributeError("{!r} has no valid result".format(self)) + return self._result + + @classmethod + def from_call(cls, func, when, reraise=None): #: context of invocation: one of "setup", "call", #: "teardown", "memocollect" - self.when = when - self.start = time() + start = time() + excinfo = None try: - self.result = func() - except KeyboardInterrupt: - if treat_keyboard_interrupt_as_exception: - self.excinfo = ExceptionInfo() - else: - self.stop = time() - raise + result = func() except: # noqa - self.excinfo = ExceptionInfo() - self.stop = time() + excinfo = ExceptionInfo.from_current() + if reraise is not None and excinfo.errisinstance(reraise): + raise + result = None + stop = time() + return cls(start=start, stop=stop, when=when, result=result, excinfo=excinfo) def __repr__(self): - if self.excinfo: - status = "exception: %s" % str(self.excinfo.value) + if self.excinfo is not None: + status = "exception" + value = self.excinfo.value else: - result = getattr(self, "result", "") - status = "result: %r" % (result,) - return "" % (self.when, status) + # TODO: investigate unification + value = repr(self._result) + status = "result" + return "".format( + when=self.when, value=value, status=status + ) def pytest_runtest_makereport(item, call): @@ -269,7 +286,7 @@ def pytest_runtest_makereport(item, call): def pytest_make_collect_report(collector): - call = CallInfo(lambda: list(collector.collect()), "collect") + call = CallInfo.from_call(lambda: list(collector.collect()), "collect") longrepr = None if not call.excinfo: outcome = "passed" diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 6f3893653..bea02306b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -167,7 +167,7 @@ def getreportopt(config): if char not in reportopts and char != "a": reportopts += char elif char == "a": - reportopts = "fEsxXw" + reportopts = "sxXwEf" return reportopts @@ -186,20 +186,17 @@ def pytest_report_teststatus(report): @attr.s class WarningReport(object): """ - Simple structure to hold warnings information captured by ``pytest_logwarning`` and ``pytest_warning_captured``. + Simple structure to hold warnings information captured by ``pytest_warning_captured``. :ivar str message: user friendly message about the warning :ivar str|None nodeid: node id that generated the warning (see ``get_location``). :ivar tuple|py.path.local fslocation: file system location of the source of the warning (see ``get_location``). - - :ivar bool legacy: if this warning report was generated from the deprecated ``pytest_logwarning`` hook. """ message = attr.ib() nodeid = attr.ib(default=None) fslocation = attr.ib(default=None) - legacy = attr.ib(default=False) def get_location(self, config): """ @@ -329,13 +326,6 @@ class TerminalReporter(object): self.write_line("INTERNALERROR> " + line) return 1 - def pytest_logwarning(self, fslocation, message, nodeid): - warnings = self.stats.setdefault("warnings", []) - warning = WarningReport( - fslocation=fslocation, message=message, nodeid=nodeid, legacy=True - ) - warnings.append(warning) - def pytest_warning_captured(self, warning_message, item): # from _pytest.nodes import get_fslocation_from_item from _pytest.warnings import warning_record_to_str @@ -621,6 +611,10 @@ class TerminalReporter(object): continue indent = (len(stack) - 1) * " " self._tw.line("%s%s" % (indent, col)) + if self.config.option.verbose >= 1: + if hasattr(col, "_obj") and col._obj.__doc__: + for line in col._obj.__doc__.strip().splitlines(): + self._tw.line("%s%s" % (indent + " ", line.strip())) @pytest.hookimpl(hookwrapper=True) def pytest_sessionfinish(self, exitstatus): diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index a38a60d8e..4a886c2e1 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -14,8 +14,6 @@ from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.python import Class from _pytest.python import Function -from _pytest.python import Module -from _pytest.python import transfer_markers def pytest_pycollect_makeitem(collector, name, obj): @@ -54,14 +52,12 @@ class UnitTestCase(Class): return self.session._fixturemanager.parsefactories(self, unittest=True) loader = TestLoader() - module = self.getparent(Module).obj foundsomething = False for name in loader.getTestCaseNames(self.obj): x = getattr(self.obj, name) if not getattr(x, "__test__", True): continue funcobj = getimfunc(x) - transfer_markers(funcobj, cls, module) yield TestCaseFunction(name, parent=self, callobj=funcobj) foundsomething = True @@ -115,6 +111,10 @@ class TestCaseFunction(Function): rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) try: excinfo = _pytest._code.ExceptionInfo(rawexcinfo) + # invoke the attributes to trigger storing the traceback + # trial causes some issue there + excinfo.value + excinfo.traceback except TypeError: try: try: @@ -136,7 +136,7 @@ class TestCaseFunction(Function): except KeyboardInterrupt: raise except fail.Exception: - excinfo = _pytest._code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo.from_current() self.__dict__.setdefault("_excinfo", []).append(excinfo) def addError(self, testcase, rawexcinfo): diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index e3e206933..764985736 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -160,19 +160,19 @@ def pytest_terminal_summary(terminalreporter): yield -def _issue_config_warning(warning, config, stacklevel): +def _issue_warning_captured(warning, hook, stacklevel): """ This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891. :param warning: the warning instance. - :param config: + :param hook: the hook caller :param stacklevel: stacklevel forwarded to warnings.warn """ with warnings.catch_warnings(record=True) as records: warnings.simplefilter("always", type(warning)) warnings.warn(warning, stacklevel=stacklevel) - config.hook.pytest_warning_captured.call_historic( + hook.pytest_warning_captured.call_historic( kwargs=dict(warning_message=records[0], when="config", item=None) ) diff --git a/src/pytest.py b/src/pytest.py index 14ed1acaa..c0010f166 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -28,7 +28,6 @@ from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.python import Class from _pytest.python import Function -from _pytest.python import Generator from _pytest.python import Instance from _pytest.python import Module from _pytest.python import Package @@ -57,7 +56,6 @@ __all__ = [ "fixture", "freeze_includes", "Function", - "Generator", "hookimpl", "hookspec", "importorskip", diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index b23cd7ca8..b7f914335 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -146,6 +146,7 @@ class TestGeneralUsage(object): assert result.ret result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)]) + @pytest.mark.filterwarnings("default") def test_better_reporting_on_conftest_load_failure(self, testdir, request): """Show a user-friendly traceback on conftest import failures (#486, #3332)""" testdir.makepyfile("") @@ -299,7 +300,7 @@ class TestGeneralUsage(object): """ import pytest def pytest_generate_tests(metafunc): - metafunc.addcall({'x': 3}, id='hello-123') + metafunc.parametrize('x', [3], ids=['hello-123']) def pytest_runtest_setup(item): print(item.keywords) if 'hello-123' in item.keywords: @@ -316,8 +317,7 @@ class TestGeneralUsage(object): p = testdir.makepyfile( """ def pytest_generate_tests(metafunc): - metafunc.addcall({'i': 1}, id="1") - metafunc.addcall({'i': 2}, id="2") + metafunc.parametrize('i', [1, 2], ids=["1", "2"]) def test_func(i): pass """ @@ -560,12 +560,11 @@ class TestInvocationVariants(object): def test_equivalence_pytest_pytest(self): assert pytest.main == py.test.cmdline.main - def test_invoke_with_string(self, capsys): - retcode = pytest.main("-h") - assert not retcode - out, err = capsys.readouterr() - assert "--help" in out - pytest.raises(ValueError, lambda: pytest.main(0)) + def test_invoke_with_invalid_type(self, capsys): + with pytest.raises( + TypeError, match="expected to be a list or tuple of strings, got: '-h'" + ): + pytest.main("-h") def test_invoke_with_path(self, tmpdir, capsys): retcode = pytest.main(tmpdir) diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 20ca0bfce..3362d4604 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -37,7 +37,7 @@ def test_code_with_class(): class A(object): pass - pytest.raises(TypeError, "_pytest._code.Code(A)") + pytest.raises(TypeError, _pytest._code.Code, A) def x(): @@ -169,7 +169,7 @@ class TestExceptionInfo(object): else: assert False except AssertionError: - exci = _pytest._code.ExceptionInfo() + exci = _pytest._code.ExceptionInfo.from_current() assert exci.getrepr() @@ -181,7 +181,7 @@ class TestTracebackEntry(object): else: assert False except AssertionError: - exci = _pytest._code.ExceptionInfo() + exci = _pytest._code.ExceptionInfo.from_current() entry = exci.traceback[0] source = entry.getsource() assert len(source) == 6 diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index c8f4c904d..4e36fb946 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -71,7 +71,7 @@ def test_excinfo_simple(): try: raise ValueError except ValueError: - info = _pytest._code.ExceptionInfo() + info = _pytest._code.ExceptionInfo.from_current() assert info.type == ValueError @@ -85,7 +85,7 @@ def test_excinfo_getstatement(): try: f() except ValueError: - excinfo = _pytest._code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo.from_current() linenumbers = [ _pytest._code.getrawcode(f).co_firstlineno - 1 + 4, _pytest._code.getrawcode(f).co_firstlineno - 1 + 1, @@ -126,7 +126,7 @@ class TestTraceback_f_g_h(object): try: h() except ValueError: - self.excinfo = _pytest._code.ExceptionInfo() + self.excinfo = _pytest._code.ExceptionInfo.from_current() def test_traceback_entries(self): tb = self.excinfo.traceback @@ -163,7 +163,7 @@ class TestTraceback_f_g_h(object): try: exec(source.compile()) except NameError: - tb = _pytest._code.ExceptionInfo().traceback + tb = _pytest._code.ExceptionInfo.from_current().traceback print(tb[-1].getsource()) s = str(tb[-1].getsource()) assert s.startswith("def xyz():\n try:") @@ -180,7 +180,8 @@ class TestTraceback_f_g_h(object): def test_traceback_cut_excludepath(self, testdir): p = testdir.makepyfile("def f(): raise ValueError") - excinfo = pytest.raises(ValueError, "p.pyimport().f()") + with pytest.raises(ValueError) as excinfo: + p.pyimport().f() basedir = py.path.local(pytest.__file__).dirpath() newtraceback = excinfo.traceback.cut(excludepath=basedir) for x in newtraceback: @@ -336,7 +337,8 @@ class TestTraceback_f_g_h(object): def test_excinfo_exconly(): excinfo = pytest.raises(ValueError, h) assert excinfo.exconly().startswith("ValueError") - excinfo = pytest.raises(ValueError, "raise ValueError('hello\\nworld')") + with pytest.raises(ValueError) as excinfo: + raise ValueError("hello\nworld") msg = excinfo.exconly(tryshort=True) assert msg.startswith("ValueError") assert msg.endswith("world") @@ -356,6 +358,12 @@ def test_excinfo_str(): assert len(s.split(":")) >= 3 # on windows it's 4 +def test_excinfo_for_later(): + e = ExceptionInfo.for_later() + assert "for raises" in repr(e) + assert "for raises" in str(e) + + def test_excinfo_errisinstance(): excinfo = pytest.raises(ValueError, h) assert excinfo.errisinstance(ValueError) @@ -365,7 +373,7 @@ def test_excinfo_no_sourcecode(): try: exec("raise ValueError()") except ValueError: - excinfo = _pytest._code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo.from_current() s = str(excinfo.traceback[-1]) assert s == " File '':1 in \n ???\n" @@ -390,7 +398,7 @@ def test_entrysource_Queue_example(): try: queue.Queue().get(timeout=0.001) except queue.Empty: - excinfo = _pytest._code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo.from_current() entry = excinfo.traceback[-1] source = entry.getsource() assert source is not None @@ -402,7 +410,7 @@ def test_codepath_Queue_example(): try: queue.Queue().get(timeout=0.001) except queue.Empty: - excinfo = _pytest._code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo.from_current() entry = excinfo.traceback[-1] path = entry.path assert isinstance(path, py.path.local) @@ -453,7 +461,7 @@ class TestFormattedExcinfo(object): except KeyboardInterrupt: raise except: # noqa - return _pytest._code.ExceptionInfo() + return _pytest._code.ExceptionInfo.from_current() assert 0, "did not raise" def test_repr_source(self): @@ -491,7 +499,7 @@ class TestFormattedExcinfo(object): try: exec(co) except ValueError: - excinfo = _pytest._code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo.from_current() repr = pr.repr_excinfo(excinfo) assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" if sys.version_info[0] >= 3: @@ -510,7 +518,7 @@ raise ValueError() try: exec(co) except ValueError: - excinfo = _pytest._code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo.from_current() repr = pr.repr_excinfo(excinfo) assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" if sys.version_info[0] >= 3: @@ -1340,7 +1348,7 @@ def test_repr_traceback_with_unicode(style, encoding): try: raise RuntimeError(msg) except RuntimeError: - e_info = ExceptionInfo() + e_info = ExceptionInfo.from_current() formatter = FormattedExcinfo(style=style) repr_traceback = formatter.repr_traceback(e_info) assert repr_traceback is not None diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 3ee46c1b8..0103acb70 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -6,6 +6,7 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +import ast import inspect import sys @@ -14,7 +15,6 @@ import six import _pytest._code import pytest from _pytest._code import Source -from _pytest._code.source import ast astonly = pytest.mark.nothing @@ -306,8 +306,6 @@ class TestSourceParsingAndCompiling(object): pytest.raises(SyntaxError, lambda: source.getstatementrange(0)) def test_compile_to_ast(self): - import ast - source = Source("x = 4") mod = source.compile(flag=ast.PyCF_ONLY_AST) assert isinstance(mod, ast.Module) @@ -317,10 +315,9 @@ class TestSourceParsingAndCompiling(object): co = self.source.compile() six.exec_(co, globals()) f(7) - excinfo = pytest.raises(AssertionError, "f(6)") + excinfo = pytest.raises(AssertionError, f, 6) frame = excinfo.traceback[-1].frame stmt = frame.code.fullsource.getstatement(frame.lineno) - # print "block", str(block) assert str(stmt).strip().startswith("assert") @pytest.mark.parametrize("name", ["", None, "my"]) @@ -361,17 +358,13 @@ def test_getline_finally(): def c(): pass - excinfo = pytest.raises( - TypeError, - """ - teardown = None - try: - c(1) - finally: - if teardown: - teardown() - """, - ) + with pytest.raises(TypeError) as excinfo: + teardown = None + try: + c(1) + finally: + if teardown: + teardown() source = excinfo.traceback[-1].statement assert str(source).strip() == "c(1)" diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index bc120b263..a6ef4739b 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -10,122 +10,7 @@ from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG pytestmark = pytest.mark.pytester_example_path("deprecated") -def test_yield_tests_deprecation(testdir): - testdir.makepyfile( - """ - def func1(arg, arg2): - assert arg == arg2 - def test_gen(): - yield "m1", func1, 15, 3*5 - yield "m2", func1, 42, 6*7 - def test_gen2(): - for k in range(10): - yield func1, 1, 1 - """ - ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines( - [ - "*test_yield_tests_deprecation.py:3:*yield tests are deprecated*", - "*test_yield_tests_deprecation.py:6:*yield tests are deprecated*", - "*2 passed*", - ] - ) - assert result.stdout.str().count("yield tests are deprecated") == 2 - - -def test_compat_properties_deprecation(testdir): - testdir.makepyfile( - """ - def test_foo(request): - print(request.node.Module) - """ - ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines( - [ - "*test_compat_properties_deprecation.py:2:*usage of Function.Module is deprecated, " - "please use pytest.Module instead*", - "*1 passed, 1 warnings in*", - ] - ) - - -def test_cached_setup_deprecation(testdir): - testdir.makepyfile( - """ - import pytest - @pytest.fixture - def fix(request): - return request.cached_setup(lambda: 1) - - def test_foo(fix): - assert fix == 1 - """ - ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines( - [ - "*test_cached_setup_deprecation.py:4:*cached_setup is deprecated*", - "*1 passed, 1 warnings in*", - ] - ) - - -def test_custom_class_deprecation(testdir): - testdir.makeconftest( - """ - import pytest - - class MyModule(pytest.Module): - - class Class(pytest.Class): - pass - - def pytest_pycollect_makemodule(path, parent): - return MyModule(path, parent) - """ - ) - testdir.makepyfile( - """ - class Test: - def test_foo(self): - pass - """ - ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines( - [ - '*test_custom_class_deprecation.py:1:*"Class" objects in collectors of type "MyModule*', - "*1 passed, 1 warnings in*", - ] - ) - - -def test_funcarg_prefix_deprecation(testdir): - testdir.makepyfile( - """ - def pytest_funcarg__value(): - return 10 - - def test_funcarg_prefix(value): - assert value == 10 - """ - ) - result = testdir.runpytest("-ra", SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines( - [ - ( - "*test_funcarg_prefix_deprecation.py:1: *pytest_funcarg__value: " - 'declaring fixtures using "pytest_funcarg__" prefix is deprecated*' - ), - "*1 passed*", - ] - ) - - -@pytest.mark.filterwarnings("default") -def test_pytest_setup_cfg_deprecated(testdir): +def test_pytest_setup_cfg_unsupported(testdir): testdir.makefile( ".cfg", setup=""" @@ -133,14 +18,11 @@ def test_pytest_setup_cfg_deprecated(testdir): addopts = --verbose """, ) - result = testdir.runpytest() - result.stdout.fnmatch_lines( - ["*pytest*section in setup.cfg files is deprecated*use*tool:pytest*instead*"] - ) + with pytest.raises(pytest.fail.Exception): + testdir.runpytest() -@pytest.mark.filterwarnings("default") -def test_pytest_custom_cfg_deprecated(testdir): +def test_pytest_custom_cfg_unsupported(testdir): testdir.makefile( ".cfg", custom=""" @@ -148,29 +30,8 @@ def test_pytest_custom_cfg_deprecated(testdir): addopts = --verbose """, ) - result = testdir.runpytest("-c", "custom.cfg") - result.stdout.fnmatch_lines( - ["*pytest*section in custom.cfg files is deprecated*use*tool:pytest*instead*"] - ) - - -def test_str_args_deprecated(tmpdir): - """Deprecate passing strings to pytest.main(). Scheduled for removal in pytest-4.0.""" - from _pytest.main import EXIT_NOTESTSCOLLECTED - - warnings = [] - - class Collect(object): - def pytest_warning_captured(self, warning_message): - warnings.append(str(warning_message.message)) - - ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()]) - msg = ( - "passing a string to pytest.main() is deprecated, " - "pass a list of arguments instead." - ) - assert msg in warnings - assert ret == EXIT_NOTESTSCOLLECTED + with pytest.raises(pytest.fail.Exception): + testdir.runpytest("-c", "custom.cfg") def test_getfuncargvalue_is_deprecated(request): @@ -191,29 +52,12 @@ def test_resultlog_is_deprecated(testdir): result = testdir.runpytest("--result-log=%s" % testdir.tmpdir.join("result.log")) result.stdout.fnmatch_lines( [ - "*--result-log is deprecated and scheduled for removal in pytest 4.0*", - "*See https://docs.pytest.org/en/latest/usage.html#creating-resultlog-format-files for more information*", + "*--result-log is deprecated and scheduled for removal in pytest 5.0*", + "*See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information*", ] ) -def test_metafunc_addcall_deprecated(testdir): - testdir.makepyfile( - """ - def pytest_generate_tests(metafunc): - metafunc.addcall({'i': 1}) - metafunc.addcall({'i': 2}) - def test_func(i): - pass - """ - ) - res = testdir.runpytest("-s", SHOW_PYTEST_WARNINGS_ARG) - assert res.ret == 0 - res.stdout.fnmatch_lines( - ["*Metafunc.addcall is deprecated*", "*2 passed, 2 warnings*"] - ) - - def test_terminal_reporter_writer_attr(pytestconfig): """Check that TerminalReporter._tw is also available as 'writer' (#2984) This attribute is planned to be deprecated in 3.4. @@ -229,6 +73,7 @@ def test_terminal_reporter_writer_attr(pytestconfig): @pytest.mark.parametrize("plugin", ["catchlog", "capturelog"]) +@pytest.mark.filterwarnings("default") def test_pytest_catchlog_deprecated(testdir, plugin): testdir.makepyfile( """ @@ -245,6 +90,12 @@ def test_pytest_catchlog_deprecated(testdir, plugin): ) +def test_raises_message_argument_deprecated(): + with pytest.warns(pytest.PytestDeprecationWarning): + with pytest.raises(RuntimeError, message="foobar"): + raise RuntimeError + + def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir): from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST @@ -262,17 +113,15 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir): """ ) res = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - assert res.ret == 0 + assert res.ret == 2 msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] res.stdout.fnmatch_lines( - "*subdirectory{sep}conftest.py:0: RemovedInPytest4Warning: {msg}*".format( - sep=os.sep, msg=msg - ) + ["*{msg}*".format(msg=msg), "*subdirectory{sep}conftest.py*".format(sep=os.sep)] ) @pytest.mark.parametrize("use_pyargs", [True, False]) -def test_pytest_plugins_in_non_top_level_conftest_deprecated_pyargs( +def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( testdir, use_pyargs ): """When using --pyargs, do not emit the warning about non-top-level conftest warnings (#4039, #4044)""" @@ -292,7 +141,7 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_pyargs( args = ("--pyargs", "pkg") if use_pyargs else () args += (SHOW_PYTEST_WARNINGS_ARG,) res = testdir.runpytest(*args) - assert res.ret == 0 + assert res.ret == (0 if use_pyargs else 2) msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] if use_pyargs: assert msg not in res.stdout.str() @@ -300,7 +149,7 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_pyargs( res.stdout.fnmatch_lines("*{msg}*".format(msg=msg)) -def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_top_level_conftest( +def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest( testdir ): from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST @@ -309,8 +158,6 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_top_level_confte subdirectory.mkdir() testdir.makeconftest( """ - import warnings - warnings.filterwarnings('always', category=DeprecationWarning) pytest_plugins=['capture'] """ ) @@ -324,16 +171,14 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_top_level_confte ) res = testdir.runpytest_subprocess() - assert res.ret == 0 + assert res.ret == 2 msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] res.stdout.fnmatch_lines( - "*subdirectory{sep}conftest.py:0: RemovedInPytest4Warning: {msg}*".format( - sep=os.sep, msg=msg - ) + ["*{msg}*".format(msg=msg), "*subdirectory{sep}conftest.py*".format(sep=os.sep)] ) -def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_false_positives( +def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives( testdir ): from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST @@ -366,37 +211,6 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_false_positives( assert msg not in res.stdout.str() -def test_call_fixture_function_deprecated(): - """Check if a warning is raised if a fixture function is called directly (#3661)""" - - @pytest.fixture - def fix(): - return 1 - - with pytest.deprecated_call(): - assert fix() == 1 - - -def test_pycollector_makeitem_is_deprecated(): - from _pytest.python import PyCollector - from _pytest.warning_types import RemovedInPytest4Warning - - class PyCollectorMock(PyCollector): - """evil hack""" - - def __init__(self): - self.called = False - - def _makeitem(self, *k): - """hack to disable the actual behaviour""" - self.called = True - - collector = PyCollectorMock() - with pytest.warns(RemovedInPytest4Warning): - collector.makeitem("foo", "bar") - assert collector.called - - def test_fixture_named_request(testdir): testdir.copy_example() result = testdir.runpytest() diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_dataclasses.py new file mode 100644 index 000000000..3bbebe2aa --- /dev/null +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from dataclasses import field + + +def test_dataclasses(): + @dataclass + class SimpleDataObject(object): + field_a: int = field() + field_b: int = field() + + left = SimpleDataObject(1, "b") + right = SimpleDataObject(1, "c") + + assert left == right diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py new file mode 100644 index 000000000..63b9f534e --- /dev/null +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from dataclasses import field + + +def test_dataclasses_with_attribute_comparison_off(): + @dataclass + class SimpleDataObject(object): + field_a: int = field() + field_b: int = field(compare=False) + + left = SimpleDataObject(1, "b") + right = SimpleDataObject(1, "c") + + assert left == right diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py new file mode 100644 index 000000000..17835c0c3 --- /dev/null +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from dataclasses import field + + +def test_dataclasses_verbose(): + @dataclass + class SimpleDataObject(object): + field_a: int = field() + field_b: int = field() + + left = SimpleDataObject(1, "b") + right = SimpleDataObject(1, "c") + + assert left == right diff --git a/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py new file mode 100644 index 000000000..24f185d8a --- /dev/null +++ b/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from dataclasses import field + + +def test_comparing_two_different_data_classes(): + @dataclass + class SimpleDataObjectOne(object): + field_a: int = field() + field_b: int = field() + + @dataclass + class SimpleDataObjectTwo(object): + field_a: int = field() + field_b: int = field() + + left = SimpleDataObjectOne(1, "b") + right = SimpleDataObjectTwo(1, "c") + + assert left != right diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py index c37045454..00981c5dc 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py @@ -3,4 +3,4 @@ import pytest @pytest.fixture def arg2(request): - pytest.raises(Exception, "request.getfixturevalue('arg1')") + pytest.raises(Exception, request.getfixturevalue, "arg1") diff --git a/testing/python/approx.py b/testing/python/approx.py index 96433d52b..26e6a4ab2 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -496,3 +496,14 @@ class TestApprox(object): assert actual != approx(expected, rel=5e-8, abs=0) assert approx(expected, rel=5e-7, abs=0) == actual assert approx(expected, rel=5e-8, abs=0) != actual + + def test_generic_sized_iterable_object(self): + class MySizedIterable(object): + def __iter__(self): + return iter([1, 2, 3, 4]) + + def __len__(self): + return 4 + + expected = MySizedIterable() + assert [1, 2, 3, 4] == approx(expected) diff --git a/testing/python/collect.py b/testing/python/collect.py index 2e5d62dd5..3147ee9e2 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -7,7 +7,6 @@ import _pytest._code import pytest from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.nodes import Collector -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG class TestModule(object): @@ -244,225 +243,7 @@ class TestClass(object): @pytest.mark.filterwarnings( "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead" ) -class TestGenerator(object): - def test_generative_functions(self, testdir): - modcol = testdir.getmodulecol( - """ - def func1(arg, arg2): - assert arg == arg2 - - def test_gen(): - yield func1, 17, 3*5 - yield func1, 42, 6*7 - """ - ) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, pytest.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], pytest.Function) - assert isinstance(gencolitems[1], pytest.Function) - assert gencolitems[0].name == "[0]" - assert gencolitems[0].obj.__name__ == "func1" - - def test_generative_methods(self, testdir): - modcol = testdir.getmodulecol( - """ - def func1(arg, arg2): - assert arg == arg2 - class TestGenMethods(object): - def test_gen(self): - yield func1, 17, 3*5 - yield func1, 42, 6*7 - """ - ) - gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, pytest.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], pytest.Function) - assert isinstance(gencolitems[1], pytest.Function) - assert gencolitems[0].name == "[0]" - assert gencolitems[0].obj.__name__ == "func1" - - def test_generative_functions_with_explicit_names(self, testdir): - modcol = testdir.getmodulecol( - """ - def func1(arg, arg2): - assert arg == arg2 - - def test_gen(): - yield "seventeen", func1, 17, 3*5 - yield "fortytwo", func1, 42, 6*7 - """ - ) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, pytest.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], pytest.Function) - assert isinstance(gencolitems[1], pytest.Function) - assert gencolitems[0].name == "['seventeen']" - assert gencolitems[0].obj.__name__ == "func1" - assert gencolitems[1].name == "['fortytwo']" - assert gencolitems[1].obj.__name__ == "func1" - - def test_generative_functions_unique_explicit_names(self, testdir): - # generative - modcol = testdir.getmodulecol( - """ - def func(): pass - def test_gen(): - yield "name", func - yield "name", func - """ - ) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, pytest.Generator) - pytest.raises(ValueError, "gencol.collect()") - - def test_generative_methods_with_explicit_names(self, testdir): - modcol = testdir.getmodulecol( - """ - def func1(arg, arg2): - assert arg == arg2 - class TestGenMethods(object): - def test_gen(self): - yield "m1", func1, 17, 3*5 - yield "m2", func1, 42, 6*7 - """ - ) - gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, pytest.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], pytest.Function) - assert isinstance(gencolitems[1], pytest.Function) - assert gencolitems[0].name == "['m1']" - assert gencolitems[0].obj.__name__ == "func1" - assert gencolitems[1].name == "['m2']" - assert gencolitems[1].obj.__name__ == "func1" - - def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir): - o = testdir.makepyfile( - """ - from __future__ import print_function - def test_generative_order_of_execution(): - import py, pytest - test_list = [] - expected_list = list(range(6)) - - def list_append(item): - test_list.append(item) - - def assert_order_of_execution(): - print('expected order', expected_list) - print('but got ', test_list) - assert test_list == expected_list - - for i in expected_list: - yield list_append, i - yield assert_order_of_execution - """ - ) - reprec = testdir.inline_run(o, SHOW_PYTEST_WARNINGS_ARG) - passed, skipped, failed = reprec.countoutcomes() - assert passed == 7 - assert not skipped and not failed - - def test_order_of_execution_generator_different_codeline(self, testdir): - o = testdir.makepyfile( - """ - from __future__ import print_function - def test_generative_tests_different_codeline(): - import py, pytest - test_list = [] - expected_list = list(range(3)) - - def list_append_2(): - test_list.append(2) - - def list_append_1(): - test_list.append(1) - - def list_append_0(): - test_list.append(0) - - def assert_order_of_execution(): - print('expected order', expected_list) - print('but got ', test_list) - assert test_list == expected_list - - yield list_append_0 - yield list_append_1 - yield list_append_2 - yield assert_order_of_execution - """ - ) - reprec = testdir.inline_run(o, SHOW_PYTEST_WARNINGS_ARG) - passed, skipped, failed = reprec.countoutcomes() - assert passed == 4 - assert not skipped and not failed - - def test_setupstate_is_preserved_134(self, testdir): - # yield-based tests are messy wrt to setupstate because - # during collection they already invoke setup functions - # and then again when they are run. For now, we want to make sure - # that the old 1.3.4 behaviour is preserved such that all - # yielded functions all share the same "self" instance that - # has been used during collection. - o = testdir.makepyfile( - """ - setuplist = [] - class TestClass(object): - def setup_method(self, func): - #print "setup_method", self, func - setuplist.append(self) - self.init = 42 - - def teardown_method(self, func): - self.init = None - - def test_func1(self): - pass - - def test_func2(self): - yield self.func2 - yield self.func2 - - def func2(self): - assert self.init - - def test_setuplist(): - # once for test_func2 during collection - # once for test_func1 during test run - # once for test_func2 during test run - #print setuplist - assert len(setuplist) == 3, len(setuplist) - assert setuplist[0] == setuplist[2], setuplist - assert setuplist[1] != setuplist[2], setuplist - """ - ) - reprec = testdir.inline_run(o, "-v", SHOW_PYTEST_WARNINGS_ARG) - passed, skipped, failed = reprec.countoutcomes() - assert passed == 4 - assert not skipped and not failed - - class TestFunction(object): - @pytest.fixture - def ignore_parametrized_marks_args(self): - """Provides arguments to pytester.runpytest() to ignore the warning about marks being applied directly - to parameters. - """ - return ("-W", "ignore:Applying marks directly to parameters") - def test_getmodulecollector(self, testdir): item = testdir.getitem("def test_func(): pass") modcol = item.getparent(pytest.Module) @@ -489,26 +270,34 @@ class TestFunction(object): ] ) - def test_function_equality(self, testdir, tmpdir): + @staticmethod + def make_function(testdir, **kwargs): from _pytest.fixtures import FixtureManager config = testdir.parseconfigure() session = testdir.Session(config) session._fixturemanager = FixtureManager(session) + return pytest.Function(config=config, parent=session, **kwargs) + + def test_function_equality(self, testdir, tmpdir): def func1(): pass def func2(): pass - f1 = pytest.Function( - name="name", parent=session, config=config, args=(1,), callobj=func1 - ) + f1 = self.make_function(testdir, name="name", args=(1,), callobj=func1) assert f1 == f1 - f2 = pytest.Function(name="name", config=config, callobj=func2, parent=session) + f2 = self.make_function(testdir, name="name", callobj=func2) assert f1 != f2 + def test_repr_produces_actual_test_id(self, testdir): + f = self.make_function( + testdir, name=r"test[\xe5]", callobj=self.test_repr_produces_actual_test_id + ) + assert repr(f) == r"" + def test_issue197_parametrize_emptyset(self, testdir): testdir.makepyfile( """ @@ -676,7 +465,6 @@ class TestFunction(object): rec = testdir.inline_run() rec.assertoutcome(passed=1) - @pytest.mark.filterwarnings("ignore:Applying marks directly to parameters") def test_parametrize_with_mark(self, testdir): items = testdir.getitems( """ @@ -684,7 +472,7 @@ class TestFunction(object): @pytest.mark.foo @pytest.mark.parametrize('arg', [ 1, - pytest.mark.bar(pytest.mark.baz(2)) + pytest.param(2, marks=[pytest.mark.baz, pytest.mark.bar]) ]) def test_function(arg): pass @@ -762,37 +550,37 @@ class TestFunction(object): assert colitems[2].name == "test2[a-c]" assert colitems[3].name == "test2[b-c]" - def test_parametrize_skipif(self, testdir, ignore_parametrized_marks_args): + def test_parametrize_skipif(self, testdir): testdir.makepyfile( """ import pytest m = pytest.mark.skipif('True') - @pytest.mark.parametrize('x', [0, 1, m(2)]) + @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)]) def test_skip_if(x): assert x < 2 """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *") - def test_parametrize_skip(self, testdir, ignore_parametrized_marks_args): + def test_parametrize_skip(self, testdir): testdir.makepyfile( """ import pytest m = pytest.mark.skip('') - @pytest.mark.parametrize('x', [0, 1, m(2)]) + @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)]) def test_skip(x): assert x < 2 """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() result.stdout.fnmatch_lines("* 2 passed, 1 skipped in *") - def test_parametrize_skipif_no_skip(self, testdir, ignore_parametrized_marks_args): + def test_parametrize_skipif_no_skip(self, testdir): testdir.makepyfile( """ import pytest @@ -804,40 +592,40 @@ class TestFunction(object): assert x < 2 """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() result.stdout.fnmatch_lines("* 1 failed, 2 passed in *") - def test_parametrize_xfail(self, testdir, ignore_parametrized_marks_args): + def test_parametrize_xfail(self, testdir): testdir.makepyfile( """ import pytest m = pytest.mark.xfail('True') - @pytest.mark.parametrize('x', [0, 1, m(2)]) + @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)]) def test_xfail(x): assert x < 2 """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() result.stdout.fnmatch_lines("* 2 passed, 1 xfailed in *") - def test_parametrize_passed(self, testdir, ignore_parametrized_marks_args): + def test_parametrize_passed(self, testdir): testdir.makepyfile( """ import pytest m = pytest.mark.xfail('True') - @pytest.mark.parametrize('x', [0, 1, m(2)]) + @pytest.mark.parametrize('x', [0, 1, pytest.param(2, marks=m)]) def test_xfail(x): pass """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() result.stdout.fnmatch_lines("* 2 passed, 1 xpassed in *") - def test_parametrize_xfail_passed(self, testdir, ignore_parametrized_marks_args): + def test_parametrize_xfail_passed(self, testdir): testdir.makepyfile( """ import pytest @@ -849,7 +637,7 @@ class TestFunction(object): pass """ ) - result = testdir.runpytest(*ignore_parametrized_marks_args) + result = testdir.runpytest() result.stdout.fnmatch_lines("* 3 passed in *") def test_function_original_name(self, testdir): @@ -1012,7 +800,7 @@ class TestConftestCustomization(object): modcol = testdir.getmodulecol("def _hello(): pass") values = [] monkeypatch.setattr( - pytest.Module, "makeitem", lambda self, name, obj: values.append(name) + pytest.Module, "_makeitem", lambda self, name, obj: values.append(name) ) values = modcol.collect() assert "_hello" not in values @@ -1095,7 +883,8 @@ def test_modulecol_roundtrip(testdir): class TestTracebackCutting(object): def test_skip_simple(self): - excinfo = pytest.raises(pytest.skip.Exception, 'pytest.skip("xxx")') + with pytest.raises(pytest.skip.Exception) as excinfo: + pytest.skip("xxx") assert excinfo.traceback[-1].frame.code.name == "skip" assert excinfo.traceback[-1].ishidden() @@ -1262,39 +1051,6 @@ class TestReportInfo(object): @pytest.mark.filterwarnings( "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead" ) - def test_generator_reportinfo(self, testdir): - modcol = testdir.getmodulecol( - """ - # lineno 0 - def test_gen(): - def check(x): - assert x - yield check, 3 - """ - ) - gencol = testdir.collect_by_name(modcol, "test_gen") - fspath, lineno, modpath = gencol.reportinfo() - assert fspath == modcol.fspath - assert lineno == 1 - assert modpath == "test_gen" - - genitem = gencol.collect()[0] - fspath, lineno, modpath = genitem.reportinfo() - assert fspath == modcol.fspath - assert lineno == 2 - assert modpath == "test_gen[0]" - """ - def test_func(): - pass - def test_genfunc(): - def check(x): - pass - yield check, 3 - class TestClass(object): - def test_method(self): - pass - """ - def test_reportinfo_with_nasty_getattr(self, testdir): # https://github.com/pytest-dev/pytest/issues/1204 modcol = testdir.getmodulecol( @@ -1364,54 +1120,6 @@ def test_customized_python_discovery_functions(testdir): result.stdout.fnmatch_lines(["*1 passed*"]) -def test_collector_attributes(testdir): - testdir.makeconftest( - """ - import pytest - def pytest_pycollect_makeitem(collector): - assert collector.Function == pytest.Function - assert collector.Class == pytest.Class - assert collector.Instance == pytest.Instance - assert collector.Module == pytest.Module - """ - ) - testdir.makepyfile( - """ - def test_hello(): - pass - """ - ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines(["*1 passed*"]) - - -def test_customize_through_attributes(testdir): - testdir.makeconftest( - """ - import pytest - class MyFunction(pytest.Function): - pass - class MyInstance(pytest.Instance): - Function = MyFunction - class MyClass(pytest.Class): - Instance = MyInstance - - def pytest_pycollect_makeitem(collector, name, obj): - if name.startswith("MyTestClass"): - return MyClass(name, parent=collector) - """ - ) - testdir.makepyfile( - """ - class MyTestClass(object): - def test_hello(self): - pass - """ - ) - result = testdir.runpytest("--collect-only", SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines(["*MyClass*", "*MyFunction*test_hello*"]) - - def test_unorderable_types(testdir): testdir.makepyfile( """ diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 86cd29724..b6692ac9b 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -627,25 +627,6 @@ class TestRequestBasic(object): print(ss.stack) assert teardownlist == [1] - def test_mark_as_fixture_with_prefix_and_decorator_fails(self, testdir): - testdir.makeconftest( - """ - import pytest - - @pytest.fixture - def pytest_funcarg__marked_with_prefix_and_decorator(): - pass - """ - ) - result = testdir.runpytest_subprocess() - assert result.ret != 0 - result.stdout.fnmatch_lines( - [ - "*AssertionError: fixtures cannot have*@pytest.fixture*", - "*pytest_funcarg__marked_with_prefix_and_decorator*", - ] - ) - def test_request_addfinalizer_failing_setup(self, testdir): testdir.makepyfile( """ @@ -906,7 +887,8 @@ class TestRequestMarking(object): assert "skipif" not in item1.keywords req1.applymarker(pytest.mark.skipif) assert "skipif" in item1.keywords - pytest.raises(ValueError, "req1.applymarker(42)") + with pytest.raises(ValueError): + req1.applymarker(42) def test_accesskeywords(self, testdir): testdir.makepyfile( @@ -952,181 +934,6 @@ class TestRequestMarking(object): reprec.assertoutcome(passed=2) -class TestRequestCachedSetup(object): - def test_request_cachedsetup_defaultmodule(self, testdir): - reprec = testdir.inline_runsource( - """ - mysetup = ["hello",].pop - - import pytest - - @pytest.fixture - def something(request): - return request.cached_setup(mysetup, scope="module") - - def test_func1(something): - assert something == "hello" - class TestClass(object): - def test_func1a(self, something): - assert something == "hello" - """, - SHOW_PYTEST_WARNINGS_ARG, - ) - reprec.assertoutcome(passed=2) - - def test_request_cachedsetup_class(self, testdir): - reprec = testdir.inline_runsource( - """ - mysetup = ["hello", "hello2", "hello3"].pop - - import pytest - @pytest.fixture - def something(request): - return request.cached_setup(mysetup, scope="class") - def test_func1(something): - assert something == "hello3" - def test_func2(something): - assert something == "hello2" - class TestClass(object): - def test_func1a(self, something): - assert something == "hello" - def test_func2b(self, something): - assert something == "hello" - """, - SHOW_PYTEST_WARNINGS_ARG, - ) - reprec.assertoutcome(passed=4) - - @pytest.mark.filterwarnings("ignore:cached_setup is deprecated") - def test_request_cachedsetup_extrakey(self, testdir): - item1 = testdir.getitem("def test_func(): pass") - req1 = fixtures.FixtureRequest(item1) - values = ["hello", "world"] - - def setup(): - return values.pop() - - ret1 = req1.cached_setup(setup, extrakey=1) - ret2 = req1.cached_setup(setup, extrakey=2) - assert ret2 == "hello" - assert ret1 == "world" - ret1b = req1.cached_setup(setup, extrakey=1) - ret2b = req1.cached_setup(setup, extrakey=2) - assert ret1 == ret1b - assert ret2 == ret2b - - @pytest.mark.filterwarnings("ignore:cached_setup is deprecated") - def test_request_cachedsetup_cache_deletion(self, testdir): - item1 = testdir.getitem("def test_func(): pass") - req1 = fixtures.FixtureRequest(item1) - values = [] - - def setup(): - values.append("setup") - - def teardown(val): - values.append("teardown") - - req1.cached_setup(setup, teardown, scope="function") - assert values == ["setup"] - # artificial call of finalizer - setupstate = req1._pyfuncitem.session._setupstate - setupstate._callfinalizers(item1) - assert values == ["setup", "teardown"] - req1.cached_setup(setup, teardown, scope="function") - assert values == ["setup", "teardown", "setup"] - setupstate._callfinalizers(item1) - assert values == ["setup", "teardown", "setup", "teardown"] - - def test_request_cached_setup_two_args(self, testdir): - testdir.makepyfile( - """ - import pytest - - @pytest.fixture - def arg1(request): - return request.cached_setup(lambda: 42) - @pytest.fixture - def arg2(request): - return request.cached_setup(lambda: 17) - def test_two_different_setups(arg1, arg2): - assert arg1 != arg2 - """ - ) - result = testdir.runpytest("-v", SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines(["*1 passed*"]) - - def test_request_cached_setup_getfixturevalue(self, testdir): - testdir.makepyfile( - """ - import pytest - - @pytest.fixture - def arg1(request): - arg1 = request.getfixturevalue("arg2") - return request.cached_setup(lambda: arg1 + 1) - @pytest.fixture - def arg2(request): - return request.cached_setup(lambda: 10) - def test_two_funcarg(arg1): - assert arg1 == 11 - """ - ) - result = testdir.runpytest("-v", SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines(["*1 passed*"]) - - def test_request_cached_setup_functional(self, testdir): - testdir.makepyfile( - test_0=""" - import pytest - values = [] - @pytest.fixture - def something(request): - val = request.cached_setup(fsetup, fteardown) - return val - def fsetup(mycache=[1]): - values.append(mycache.pop()) - return values - def fteardown(something): - values.remove(something[0]) - values.append(2) - def test_list_once(something): - assert something == [1] - def test_list_twice(something): - assert something == [1] - """ - ) - testdir.makepyfile( - test_1=""" - import test_0 # should have run already - def test_check_test0_has_teardown_correct(): - assert test_0.values == [2] - """ - ) - result = testdir.runpytest("-v", SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines(["*3 passed*"]) - - def test_issue117_sessionscopeteardown(self, testdir): - testdir.makepyfile( - """ - import pytest - - @pytest.fixture - def app(request): - app = request.cached_setup( - scope='session', - setup=lambda: 0, - teardown=lambda x: 3/x) - return app - def test_func(app): - pass - """ - ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - assert result.ret != 0 - result.stdout.fnmatch_lines(["*3/x*", "*ZeroDivisionError*"]) - - class TestFixtureUsages(object): def test_noargfixturedec(self, testdir): testdir.makepyfile( @@ -1849,24 +1656,6 @@ class TestAutouseManagement(object): reprec = testdir.inline_run("-s") reprec.assertoutcome(passed=1) - def test_autouse_honored_for_yield(self, testdir): - testdir.makepyfile( - """ - import pytest - @pytest.fixture(autouse=True) - def tst(): - global x - x = 3 - def test_gen(): - def f(hello): - assert x == abs(hello) - yield f, 3 - yield f, -3 - """ - ) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) - reprec.assertoutcome(passed=2) - def test_funcarg_and_setup(self, testdir): testdir.makepyfile( """ @@ -2314,15 +2103,7 @@ class TestFixtureMarker(object): reprec = testdir.inline_run() reprec.assertoutcome(passed=4) - @pytest.mark.parametrize( - "method", - [ - 'request.getfixturevalue("arg")', - 'request.cached_setup(lambda: None, scope="function")', - ], - ids=["getfixturevalue", "cached_setup"], - ) - def test_scope_mismatch_various(self, testdir, method): + def test_scope_mismatch_various(self, testdir): testdir.makeconftest( """ import pytest @@ -2338,11 +2119,10 @@ class TestFixtureMarker(object): import pytest @pytest.fixture(scope="session") def arg(request): - %s + request.getfixturevalue("arg") def test_1(arg): pass """ - % method ) result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) assert result.ret != 0 @@ -4070,3 +3850,14 @@ class TestScopeOrdering(object): ) reprec = testdir.inline_run() reprec.assertoutcome(passed=2) + + +def test_call_fixture_function_error(): + """Check if an error is raised if a fixture function is called directly (#4545)""" + + @pytest.fixture + def fix(): + return 1 + + with pytest.raises(pytest.fail.Exception): + assert fix() == 1 diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 1a9cbf408..54a6ecb91 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -5,6 +5,7 @@ import textwrap import attr import hypothesis +import six from hypothesis import strategies import pytest @@ -53,66 +54,6 @@ class TestMetafunc(object): assert metafunc.function is func assert metafunc.cls is None - def test_addcall_no_args(self): - def func(arg1): - pass - - metafunc = self.Metafunc(func) - metafunc.addcall() - assert len(metafunc._calls) == 1 - call = metafunc._calls[0] - assert call.id == "0" - assert not hasattr(call, "param") - - def test_addcall_id(self): - def func(arg1): - pass - - metafunc = self.Metafunc(func) - pytest.raises(ValueError, "metafunc.addcall(id=None)") - - metafunc.addcall(id=1) - pytest.raises(ValueError, "metafunc.addcall(id=1)") - pytest.raises(ValueError, "metafunc.addcall(id='1')") - metafunc.addcall(id=2) - assert len(metafunc._calls) == 2 - assert metafunc._calls[0].id == "1" - assert metafunc._calls[1].id == "2" - - def test_addcall_param(self): - def func(arg1): - pass - - metafunc = self.Metafunc(func) - - class obj(object): - pass - - metafunc.addcall(param=obj) - metafunc.addcall(param=obj) - metafunc.addcall(param=1) - assert len(metafunc._calls) == 3 - assert metafunc._calls[0].getparam("arg1") == obj - assert metafunc._calls[1].getparam("arg1") == obj - assert metafunc._calls[2].getparam("arg1") == 1 - - def test_addcall_funcargs(self): - def func(x): - pass - - metafunc = self.Metafunc(func) - - class obj(object): - pass - - metafunc.addcall(funcargs={"x": 2}) - metafunc.addcall(funcargs={"x": 3}) - pytest.raises(pytest.fail.Exception, "metafunc.addcall({'xyz': 0})") - assert len(metafunc._calls) == 2 - assert metafunc._calls[0].funcargs == {"x": 2} - assert metafunc._calls[1].funcargs == {"x": 3} - assert not hasattr(metafunc._calls[1], "param") - def test_parametrize_error(self): def func(x, y): pass @@ -262,11 +203,8 @@ class TestMetafunc(object): from _pytest.python import _idval escaped = _idval(value, "a", 6, None, item=None, config=None) - assert isinstance(escaped, str) - if PY3: - escaped.encode("ascii") - else: - escaped.decode("ascii") + assert isinstance(escaped, six.text_type) + escaped.encode("ascii") def test_unicode_idval(self): """This tests that Unicode strings outside the ASCII character set get @@ -382,6 +320,34 @@ class TestMetafunc(object): "\\xc3\\xb4-other", ] + def test_idmaker_non_printable_characters(self): + from _pytest.python import idmaker + + result = idmaker( + ("s", "n"), + [ + pytest.param("\x00", 1), + pytest.param("\x05", 2), + pytest.param(b"\x00", 3), + pytest.param(b"\x05", 4), + pytest.param("\t", 5), + pytest.param(b"\t", 6), + ], + ) + assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"] + + def test_idmaker_manual_ids_must_be_printable(self): + from _pytest.python import idmaker + + result = idmaker( + ("s",), + [ + pytest.param("x00", id="hello \x00"), + pytest.param("x05", id="hello \x05"), + ], + ) + assert result == ["hello \\x00", "hello \\x05"] + def test_idmaker_enum(self): from _pytest.python import idmaker @@ -427,7 +393,6 @@ class TestMetafunc(object): ) assert result == ["a-a0", "a-a1", "a-a2"] - @pytest.mark.filterwarnings("default") def test_parametrize_ids_exception(self, testdir): """ :param testdir: the instance of Testdir class, a temporary @@ -445,14 +410,11 @@ class TestMetafunc(object): pass """ ) - result = testdir.runpytest("--collect-only", SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest() result.stdout.fnmatch_lines( [ - "", - " ", - " ", - "*test_parametrize_ids_exception.py:6: *parameter arg at position 0*", - "*test_parametrize_ids_exception.py:6: *parameter arg at position 1*", + "*test_foo: error raised while trying to determine id of parameter 'arg' at position 0", + "*Exception: bad ids", ] ) @@ -482,19 +444,6 @@ class TestMetafunc(object): ) assert result == ["a0", "a1", "b0", "c", "b1"] - def test_addcall_and_parametrize(self): - def func(x, y): - pass - - metafunc = self.Metafunc(func) - metafunc.addcall({"x": 1}) - metafunc.parametrize("y", [2, 3]) - assert len(metafunc._calls) == 2 - assert metafunc._calls[0].funcargs == {"x": 1, "y": 2} - assert metafunc._calls[1].funcargs == {"x": 1, "y": 3} - assert metafunc._calls[0].id == "0-2" - assert metafunc._calls[1].id == "0-3" - @pytest.mark.issue714 def test_parametrize_indirect(self): def func(x, y): @@ -684,20 +633,6 @@ class TestMetafunc(object): ["*already takes an argument 'y' with a default value"] ) - def test_addcalls_and_parametrize_indirect(self): - def func(x, y): - pass - - metafunc = self.Metafunc(func) - metafunc.addcall(param="123") - metafunc.parametrize("x", [1], indirect=True) - metafunc.parametrize("y", [2, 3], indirect=True) - assert len(metafunc._calls) == 2 - assert metafunc._calls[0].funcargs == {} - assert metafunc._calls[1].funcargs == {} - assert metafunc._calls[0].params == dict(x=1, y=2) - assert metafunc._calls[1].params == dict(x=1, y=3) - def test_parametrize_functional(self, testdir): testdir.makepyfile( """ @@ -845,7 +780,7 @@ class TestMetafuncFunctional(object): # assumes that generate/provide runs in the same process import sys, pytest, six def pytest_generate_tests(metafunc): - metafunc.addcall(param=metafunc) + metafunc.parametrize('metafunc', [metafunc]) @pytest.fixture def metafunc(request): @@ -870,43 +805,15 @@ class TestMetafuncFunctional(object): result = testdir.runpytest(p, "-v", SHOW_PYTEST_WARNINGS_ARG) result.assert_outcomes(passed=2) - def test_addcall_with_two_funcargs_generators(self, testdir): - testdir.makeconftest( - """ - def pytest_generate_tests(metafunc): - assert "arg1" in metafunc.fixturenames - metafunc.addcall(funcargs=dict(arg1=1, arg2=2)) - """ - ) - p = testdir.makepyfile( - """ - def pytest_generate_tests(metafunc): - metafunc.addcall(funcargs=dict(arg1=1, arg2=1)) - - class TestClass(object): - def test_myfunc(self, arg1, arg2): - assert arg1 == arg2 - """ - ) - result = testdir.runpytest("-v", p, SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines( - ["*test_myfunc*0*PASS*", "*test_myfunc*1*FAIL*", "*1 failed, 1 passed*"] - ) - def test_two_functions(self, testdir): p = testdir.makepyfile( """ def pytest_generate_tests(metafunc): - metafunc.addcall(param=10) - metafunc.addcall(param=20) - - import pytest - @pytest.fixture - def arg1(request): - return request.param + metafunc.parametrize('arg1', [10, 20], ids=['0', '1']) def test_func1(arg1): assert arg1 == 10 + def test_func2(arg1): assert arg1 in (10, 20) """ @@ -917,6 +824,7 @@ class TestMetafuncFunctional(object): "*test_func1*0*PASS*", "*test_func1*1*FAIL*", "*test_func2*PASS*", + "*test_func2*PASS*", "*1 failed, 3 passed*", ] ) @@ -935,47 +843,12 @@ class TestMetafuncFunctional(object): result = testdir.runpytest(p) result.assert_outcomes(passed=1) - def test_generate_plugin_and_module(self, testdir): - testdir.makeconftest( - """ - def pytest_generate_tests(metafunc): - assert "arg1" in metafunc.fixturenames - metafunc.addcall(id="world", param=(2,100)) - """ - ) - p = testdir.makepyfile( - """ - def pytest_generate_tests(metafunc): - metafunc.addcall(param=(1,1), id="hello") - - import pytest - @pytest.fixture - def arg1(request): - return request.param[0] - @pytest.fixture - def arg2(request): - return request.param[1] - - class TestClass(object): - def test_myfunc(self, arg1, arg2): - assert arg1 == arg2 - """ - ) - result = testdir.runpytest("-v", p, SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines( - [ - "*test_myfunc*hello*PASS*", - "*test_myfunc*world*FAIL*", - "*1 failed, 1 passed*", - ] - ) - def test_generate_tests_in_class(self, testdir): p = testdir.makepyfile( """ class TestClass(object): def pytest_generate_tests(self, metafunc): - metafunc.addcall(funcargs={'hello': 'world'}, id="hello") + metafunc.parametrize('hello', ['world'], ids=['hellow']) def test_myfunc(self, hello): assert hello == "world" @@ -988,8 +861,7 @@ class TestMetafuncFunctional(object): p = testdir.makepyfile( """ def pytest_generate_tests(metafunc): - metafunc.addcall({'arg1': 10}) - metafunc.addcall({'arg1': 20}) + metafunc.parametrize('arg1', [10, 20], ids=["0", "1"]) class TestClass(object): def test_func(self, arg1): @@ -1006,7 +878,7 @@ class TestMetafuncFunctional(object): p = testdir.makepyfile( """ def pytest_generate_tests(metafunc): - metafunc.addcall({'arg1': 1}) + metafunc.parametrize('arg1', [1]) class TestClass(object): def test_method(self, arg1): @@ -1500,7 +1372,6 @@ class TestMetafuncFunctionalAuto(object): assert output.count("preparing foo-3") == 1 -@pytest.mark.filterwarnings("ignore:Applying marks directly to parameters") @pytest.mark.issue308 class TestMarkersWithParametrization(object): def test_simple_mark(self, testdir): @@ -1510,7 +1381,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.foo @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.bar((1, 3)), + pytest.param(1, 3, marks=pytest.mark.bar), (2, 3), ]) def test_increment(n, expected): @@ -1530,7 +1401,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.foo((2, 3)), + pytest.param(2, 3, marks=pytest.mark.foo), (3, 4), ]) def test_increment(n, expected): @@ -1570,7 +1441,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.xfail((1, 3)), + pytest.param(1, 3, marks=pytest.mark.xfail), (2, 3), ]) def test_increment(n, expected): @@ -1587,7 +1458,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize("n", [ 2, - pytest.mark.xfail(3), + pytest.param(3, marks=pytest.mark.xfail), 4, ]) def test_isEven(n): @@ -1603,7 +1474,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.xfail("True")((1, 3)), + pytest.param(1, 3, marks=pytest.mark.xfail("True")), (2, 3), ]) def test_increment(n, expected): @@ -1619,7 +1490,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.xfail(reason="some bug")((1, 3)), + pytest.param(1, 3, marks=pytest.mark.xfail(reason="some bug")), (2, 3), ]) def test_increment(n, expected): @@ -1635,7 +1506,7 @@ class TestMarkersWithParametrization(object): @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.xfail("True", reason="some bug")((1, 3)), + pytest.param(1, 3, marks=pytest.mark.xfail("True", reason="some bug")), (2, 3), ]) def test_increment(n, expected): @@ -1650,9 +1521,11 @@ class TestMarkersWithParametrization(object): s = """ import pytest + m = pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict}) + @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.xfail("sys.version_info > (0, 0, 0)", reason="some bug", strict={strict})((2, 3)), + pytest.param(2, 3, marks=m), (3, 4), ]) def test_increment(n, expected): @@ -1676,7 +1549,7 @@ class TestMarkersWithParametrization(object): failingTestData = [(1, 3), (2, 2)] - testData = passingTestData + [pytest.mark.xfail(d) + testData = passingTestData + [pytest.param(*d, marks=pytest.mark.xfail) for d in failingTestData] metafunc.parametrize(("n", "expected"), testData) diff --git a/testing/python/raises.py b/testing/python/raises.py index 2ee18b173..4ff0b51bc 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -4,25 +4,32 @@ import six import pytest from _pytest.outcomes import Failed +from _pytest.warning_types import PytestDeprecationWarning class TestRaises(object): def test_raises(self): source = "int('qwe')" - excinfo = pytest.raises(ValueError, source) + with pytest.warns(PytestDeprecationWarning): + excinfo = pytest.raises(ValueError, source) code = excinfo.traceback[-1].frame.code s = str(code.fullsource) assert s == source def test_raises_exec(self): - pytest.raises(ValueError, "a,x = []") + with pytest.warns(PytestDeprecationWarning) as warninfo: + pytest.raises(ValueError, "a,x = []") + assert warninfo[0].filename == __file__ def test_raises_exec_correct_filename(self): - excinfo = pytest.raises(ValueError, 'int("s")') - assert __file__ in excinfo.traceback[-1].path + with pytest.warns(PytestDeprecationWarning): + excinfo = pytest.raises(ValueError, 'int("s")') + assert __file__ in excinfo.traceback[-1].path def test_raises_syntax_error(self): - pytest.raises(SyntaxError, "qwe qwe qwe") + with pytest.warns(PytestDeprecationWarning) as warninfo: + pytest.raises(SyntaxError, "qwe qwe qwe") + assert warninfo[0].filename == __file__ def test_raises_function(self): pytest.raises(ValueError, int, "hello") @@ -119,8 +126,9 @@ class TestRaises(object): def test_custom_raise_message(self): message = "TEST_MESSAGE" try: - with pytest.raises(ValueError, message=message): - pass + with pytest.warns(PytestDeprecationWarning): + with pytest.raises(ValueError, message=message): + pass except pytest.raises.Exception as e: assert e.msg == message else: diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 6f5852e54..fcefdbb11 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -6,6 +6,7 @@ from __future__ import print_function import sys import textwrap +import attr import py import six @@ -549,6 +550,115 @@ class TestAssert_reprcompare(object): assert msg +class TestAssert_reprcompare_dataclass(object): + @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") + def test_dataclasses(self, testdir): + p = testdir.copy_example("dataclasses/test_compare_dataclasses.py") + result = testdir.runpytest(p) + result.assert_outcomes(failed=1, passed=0) + result.stdout.fnmatch_lines( + [ + "*Omitting 1 identical items, use -vv to show*", + "*Differing attributes:*", + "*field_b: 'b' != 'c'*", + ] + ) + + @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") + def test_dataclasses_verbose(self, testdir): + p = testdir.copy_example("dataclasses/test_compare_dataclasses_verbose.py") + result = testdir.runpytest(p, "-vv") + result.assert_outcomes(failed=1, passed=0) + result.stdout.fnmatch_lines( + [ + "*Matching attributes:*", + "*['field_a']*", + "*Differing attributes:*", + "*field_b: 'b' != 'c'*", + ] + ) + + @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") + def test_dataclasses_with_attribute_comparison_off(self, testdir): + p = testdir.copy_example( + "dataclasses/test_compare_dataclasses_field_comparison_off.py" + ) + result = testdir.runpytest(p, "-vv") + result.assert_outcomes(failed=0, passed=1) + + @pytest.mark.skipif(sys.version_info < (3, 7), reason="Dataclasses in Python3.7+") + def test_comparing_two_different_data_classes(self, testdir): + p = testdir.copy_example( + "dataclasses/test_compare_two_different_dataclasses.py" + ) + result = testdir.runpytest(p, "-vv") + result.assert_outcomes(failed=0, passed=1) + + +class TestAssert_reprcompare_attrsclass(object): + def test_attrs(self): + @attr.s + class SimpleDataObject(object): + field_a = attr.ib() + field_b = attr.ib() + + left = SimpleDataObject(1, "b") + right = SimpleDataObject(1, "c") + + lines = callequal(left, right) + assert lines[1].startswith("Omitting 1 identical item") + assert "Matching attributes" not in lines + for line in lines[1:]: + assert "field_a" not in line + + def test_attrs_verbose(self): + @attr.s + class SimpleDataObject(object): + field_a = attr.ib() + field_b = attr.ib() + + left = SimpleDataObject(1, "b") + right = SimpleDataObject(1, "c") + + lines = callequal(left, right, verbose=2) + assert lines[1].startswith("Matching attributes:") + assert "Omitting" not in lines[1] + assert lines[2] == "['field_a']" + + def test_attrs_with_attribute_comparison_off(self): + @attr.s + class SimpleDataObject(object): + field_a = attr.ib() + field_b = attr.ib(cmp=False) + + left = SimpleDataObject(1, "b") + right = SimpleDataObject(1, "b") + + lines = callequal(left, right, verbose=2) + assert lines[1].startswith("Matching attributes:") + assert "Omitting" not in lines[1] + assert lines[2] == "['field_a']" + for line in lines[2:]: + assert "field_b" not in line + + def test_comparing_two_different_attrs_classes(self): + @attr.s + class SimpleDataObjectOne(object): + field_a = attr.ib() + field_b = attr.ib() + + @attr.s + class SimpleDataObjectTwo(object): + field_a = attr.ib() + field_b = attr.ib() + + left = SimpleDataObjectOne(1, "b") + right = SimpleDataObjectTwo(1, "c") + + lines = callequal(left, right) + assert lines is None + + class TestFormatExplanation(object): def test_special_chars_full(self, testdir): # Issue 453, for the bug this would raise IndexError diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index a02433cd6..4187e365b 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -823,7 +823,9 @@ def test_rewritten(): testdir.makepyfile(test_remember_rewritten_modules="") warnings = [] hook = AssertionRewritingHook(pytestconfig) - monkeypatch.setattr(hook.config, "warn", lambda code, msg: warnings.append(msg)) + monkeypatch.setattr( + hook, "_warn_already_imported", lambda code, msg: warnings.append(msg) + ) hook.find_module("test_remember_rewritten_modules") hook.load_module("test_remember_rewritten_modules") hook.mark_rewrite("test_remember_rewritten_modules") diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 30fe23aeb..29c2d8a1d 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -925,3 +925,15 @@ def test_does_not_create_boilerplate_in_existing_dirs(testdir): assert os.path.isdir("v") # cache contents assert not os.path.exists(".gitignore") assert not os.path.exists("README.md") + + +def test_cachedir_tag(testdir): + """Ensure we automatically create CACHEDIR.TAG file in the pytest_cache directory (#4278).""" + from _pytest.cacheprovider import Cache + from _pytest.cacheprovider import CACHEDIR_TAG_CONTENT + + config = testdir.parseconfig() + cache = Cache.for_config(config) + cache.set("foo", "bar") + cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG") + assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT diff --git a/testing/test_capture.py b/testing/test_capture.py index d44b58ee0..43cd700d3 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -87,7 +87,7 @@ class TestCaptureManager(object): try: capman = CaptureManager("fd") capman.start_global_capturing() - pytest.raises(AssertionError, "capman.start_global_capturing()") + pytest.raises(AssertionError, capman.start_global_capturing) capman.stop_global_capturing() finally: capouter.stop_capturing() @@ -832,10 +832,10 @@ class TestCaptureIO(object): f = capture.CaptureIO() if sys.version_info >= (3, 0): f.write("\u00f6") - pytest.raises(TypeError, "f.write(bytes('hello', 'UTF-8'))") + pytest.raises(TypeError, f.write, b"hello") else: - f.write(text_type("\u00f6", "UTF-8")) - f.write("hello") # bytes + f.write(u"\u00f6") + f.write(b"hello") s = f.getvalue() f.close() assert isinstance(s, text_type) @@ -1183,7 +1183,7 @@ class TestStdCapture(object): print("XXX which indicates an error in the underlying capturing") print("XXX mechanisms") with self.getcapture(): - pytest.raises(IOError, "sys.stdin.read()") + pytest.raises(IOError, sys.stdin.read) class TestStdCaptureFD(TestStdCapture): diff --git a/testing/test_collection.py b/testing/test_collection.py index fae23025e..36e8a69ce 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -21,20 +21,6 @@ class TestCollector(object): assert not issubclass(Collector, Item) assert not issubclass(Item, Collector) - def test_compat_attributes(self, testdir, recwarn): - modcol = testdir.getmodulecol( - """ - def test_pass(): pass - def test_fail(): assert 0 - """ - ) - recwarn.clear() - assert modcol.Module == pytest.Module - assert modcol.Class == pytest.Class - assert modcol.Item == pytest.Item - assert modcol.File == pytest.File - assert modcol.Function == pytest.Function - def test_check_equality(self, testdir): modcol = testdir.getmodulecol( """ @@ -950,10 +936,10 @@ def test_collect_init_tests(testdir): [ "collected 2 items", "", - " ", - " ", - " ", + " ", + " ", + " ", + " ", ] ) result = testdir.runpytest("./tests", "--collect-only") @@ -961,10 +947,10 @@ def test_collect_init_tests(testdir): [ "collected 2 items", "", - " ", - " ", - " ", + " ", + " ", + " ", + " ", ] ) # Ignores duplicates with "." and pkginit (#4310). @@ -972,11 +958,11 @@ def test_collect_init_tests(testdir): result.stdout.fnmatch_lines( [ "collected 2 items", - "", - " ", - " ", - " ", - " ", + "", + " ", + " ", + " ", + " ", ] ) # Same as before, but different order. @@ -984,21 +970,21 @@ def test_collect_init_tests(testdir): result.stdout.fnmatch_lines( [ "collected 2 items", - "", - " ", - " ", - " ", - " ", + "", + " ", + " ", + " ", + " ", ] ) result = testdir.runpytest("./tests/test_foo.py", "--collect-only") result.stdout.fnmatch_lines( - ["", " ", " "] + ["", " ", " "] ) assert "test_init" not in result.stdout.str() result = testdir.runpytest("./tests/__init__.py", "--collect-only") result.stdout.fnmatch_lines( - ["", " ", " "] + ["", " ", " "] ) assert "test_foo" not in result.stdout.str() diff --git a/testing/test_config.py b/testing/test_config.py index ebd4cca83..b0b09f44a 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -12,7 +12,6 @@ from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import getcfg from _pytest.main import EXIT_NOTESTSCOLLECTED -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG class TestParseIni(object): @@ -194,7 +193,7 @@ class TestConfigAPI(object): config = testdir.parseconfig("--hello=this") for x in ("hello", "--hello", "-X"): assert config.getoption(x) == "this" - pytest.raises(ValueError, "config.getoption('qweqwe')") + pytest.raises(ValueError, config.getoption, "qweqwe") @pytest.mark.skipif("sys.version_info[0] < 3") def test_config_getoption_unicode(self, testdir): @@ -211,7 +210,7 @@ class TestConfigAPI(object): def test_config_getvalueorskip(self, testdir): config = testdir.parseconfig() - pytest.raises(pytest.skip.Exception, "config.getvalueorskip('hello')") + pytest.raises(pytest.skip.Exception, config.getvalueorskip, "hello") verbose = config.getvalueorskip("verbose") assert verbose == config.option.verbose @@ -726,7 +725,8 @@ def test_config_in_subdirectory_colon_command_line_issue2148(testdir): def test_notify_exception(testdir, capfd): config = testdir.parseconfig() - excinfo = pytest.raises(ValueError, "raise ValueError(1)") + with pytest.raises(ValueError) as excinfo: + raise ValueError(1) config.notify_exception(excinfo) out, err = capfd.readouterr() assert "ValueError" in err @@ -792,66 +792,6 @@ def test_collect_pytest_prefix_bug(pytestconfig): assert pm.parse_hookimpl_opts(Dummy(), "pytest_something") is None -class TestLegacyWarning(object): - @pytest.mark.filterwarnings("default") - def test_warn_config(self, testdir): - testdir.makeconftest( - """ - values = [] - def pytest_runtest_setup(item): - item.config.warn("C1", "hello") - def pytest_logwarning(code, message): - if message == "hello" and code == "C1": - values.append(1) - """ - ) - testdir.makepyfile( - """ - def test_proper(pytestconfig): - import conftest - assert conftest.values == [1] - """ - ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines( - ["*hello", "*config.warn has been deprecated*", "*1 passed*"] - ) - - @pytest.mark.filterwarnings("default") - @pytest.mark.parametrize("use_kw", [True, False]) - def test_warn_on_test_item_from_request(self, testdir, use_kw): - code_kw = "code=" if use_kw else "" - message_kw = "message=" if use_kw else "" - testdir.makepyfile( - """ - import pytest - - @pytest.fixture - def fix(request): - request.node.warn({code_kw}"T1", {message_kw}"hello") - - def test_hello(fix): - pass - """.format( - code_kw=code_kw, message_kw=message_kw - ) - ) - result = testdir.runpytest( - "--disable-pytest-warnings", SHOW_PYTEST_WARNINGS_ARG - ) - assert "hello" not in result.stdout.str() - - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines( - """ - ===*warnings summary*=== - *test_warn_on_test_item_from_request.py::test_hello* - *hello* - *test_warn_on_test_item_from_request.py:7:*Node.warn(code, message) form has been deprecated* - """ - ) - - class TestRootdir(object): def test_simple_noini(self, tmpdir): assert get_common_ancestor([tmpdir]) == tmpdir diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index e04af1ec3..59c11fa00 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -153,6 +153,37 @@ class TestPython(object): val = tnode["time"] assert round(float(val), 2) >= 0.03 + @pytest.mark.parametrize("duration_report", ["call", "total"]) + def test_junit_duration_report(self, testdir, monkeypatch, duration_report): + + # mock LogXML.node_reporter so it always sets a known duration to each test report object + original_node_reporter = LogXML.node_reporter + + def node_reporter_wrapper(s, report): + report.duration = 1.0 + reporter = original_node_reporter(s, report) + return reporter + + monkeypatch.setattr(LogXML, "node_reporter", node_reporter_wrapper) + + testdir.makepyfile( + """ + def test_foo(): + pass + """ + ) + result, dom = runandparse( + testdir, "-o", "junit_duration_report={}".format(duration_report) + ) + node = dom.find_first_by_tag("testsuite") + tnode = node.find_first_by_tag("testcase") + val = float(tnode["time"]) + if duration_report == "total": + assert val == 3.0 + else: + assert duration_report == "call" + assert val == 1.0 + def test_setup_error(self, testdir): testdir.makepyfile( """ diff --git a/testing/test_mark.py b/testing/test_mark.py index e32bcc395..a10e2e19d 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -5,20 +5,19 @@ from __future__ import print_function import os import sys +import six + +import pytest +from _pytest.mark import EMPTY_PARAMETERSET_OPTION +from _pytest.mark import MarkGenerator as Mark +from _pytest.nodes import Collector +from _pytest.nodes import Node from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG try: import mock except ImportError: import unittest.mock as mock -import pytest -from _pytest.mark import ( - MarkGenerator as Mark, - ParameterSet, - transfer_markers, - EMPTY_PARAMETERSET_OPTION, -) -from _pytest.nodes import Node, Collector ignore_markinfo = pytest.mark.filterwarnings( "ignore:MarkInfo objects:pytest.RemovedInPytest4Warning" @@ -26,12 +25,6 @@ ignore_markinfo = pytest.mark.filterwarnings( class TestMark(object): - def test_markinfo_repr(self): - from _pytest.mark import MarkInfo, Mark - - m = MarkInfo.for_mark(Mark("hello", (1, 2), {})) - repr(m) - @pytest.mark.parametrize("attr", ["mark", "param"]) @pytest.mark.parametrize("modulename", ["py.test", "pytest"]) def test_pytest_exists_in_namespace_all(self, attr, modulename): @@ -57,105 +50,8 @@ class TestMark(object): def test_pytest_mark_name_starts_with_underscore(self): mark = Mark() - pytest.raises(AttributeError, getattr, mark, "_some_name") - - def test_pytest_mark_bare(self): - mark = Mark() - - def f(): - pass - - mark.hello(f) - assert f.hello - - def test_mark_legacy_ignore_fail(self): - def add_attribute(func): - func.foo = 1 - return func - - @pytest.mark.foo - @add_attribute - def test_fun(): - pass - - assert test_fun.foo == 1 - assert test_fun.pytestmark - - @ignore_markinfo - def test_pytest_mark_keywords(self): - mark = Mark() - - def f(): - pass - - mark.world(x=3, y=4)(f) - assert f.world - assert f.world.kwargs["x"] == 3 - assert f.world.kwargs["y"] == 4 - - @ignore_markinfo - def test_apply_multiple_and_merge(self): - mark = Mark() - - def f(): - pass - - mark.world - mark.world(x=3)(f) - assert f.world.kwargs["x"] == 3 - mark.world(y=4)(f) - assert f.world.kwargs["x"] == 3 - assert f.world.kwargs["y"] == 4 - mark.world(y=1)(f) - assert f.world.kwargs["y"] == 1 - assert len(f.world.args) == 0 - - @ignore_markinfo - def test_pytest_mark_positional(self): - mark = Mark() - - def f(): - pass - - mark.world("hello")(f) - assert f.world.args[0] == "hello" - mark.world("world")(f) - - @ignore_markinfo - def test_pytest_mark_positional_func_and_keyword(self): - mark = Mark() - - def f(): - raise Exception - - m = mark.world(f, omega="hello") - - def g(): - pass - - assert m(g) == g - assert g.world.args[0] is f - assert g.world.kwargs["omega"] == "hello" - - @ignore_markinfo - def test_pytest_mark_reuse(self): - mark = Mark() - - def f(): - pass - - w = mark.some - w("hello", reason="123")(f) - assert f.some.args[0] == "hello" - assert f.some.kwargs["reason"] == "123" - - def g(): - pass - - w("world", reason2="456")(g) - assert g.some.args[0] == "world" - assert "reason" not in g.some.kwargs - assert g.some.kwargs["reason2"] == "456" + with pytest.raises(AttributeError): + mark._some_name def test_marked_class_run_twice(testdir, request): @@ -476,8 +372,10 @@ def test_parametrized_collect_with_wrong_args(testdir): result = testdir.runpytest(py_file) result.stdout.fnmatch_lines( [ - 'E ValueError: In "parametrize" the number of values ((1, 2, 3)) ' - "must be equal to the number of names (['foo', 'bar'])" + 'test_parametrized_collect_with_wrong_args.py::test_func: in "parametrize" the number of names (2):', + " ['foo', 'bar']", + "must be equal to the number of values (3):", + " (1, 2, 3)", ] ) @@ -503,116 +401,6 @@ def test_parametrized_with_kwargs(testdir): class TestFunctional(object): - def test_mark_per_function(self, testdir): - p = testdir.makepyfile( - """ - import pytest - @pytest.mark.hello - def test_hello(): - assert hasattr(test_hello, 'hello') - """ - ) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines(["*1 passed*"]) - - def test_mark_per_module(self, testdir): - item = testdir.getitem( - """ - import pytest - pytestmark = pytest.mark.hello - def test_func(): - pass - """ - ) - keywords = item.keywords - assert "hello" in keywords - - def test_marklist_per_class(self, testdir): - item = testdir.getitem( - """ - import pytest - class TestClass(object): - pytestmark = [pytest.mark.hello, pytest.mark.world] - def test_func(self): - assert TestClass.test_func.hello - assert TestClass.test_func.world - """ - ) - keywords = item.keywords - assert "hello" in keywords - - def test_marklist_per_module(self, testdir): - item = testdir.getitem( - """ - import pytest - pytestmark = [pytest.mark.hello, pytest.mark.world] - class TestClass(object): - def test_func(self): - assert TestClass.test_func.hello - assert TestClass.test_func.world - """ - ) - keywords = item.keywords - assert "hello" in keywords - assert "world" in keywords - - def test_mark_per_class_decorator(self, testdir): - item = testdir.getitem( - """ - import pytest - @pytest.mark.hello - class TestClass(object): - def test_func(self): - assert TestClass.test_func.hello - """ - ) - keywords = item.keywords - assert "hello" in keywords - - def test_mark_per_class_decorator_plus_existing_dec(self, testdir): - item = testdir.getitem( - """ - import pytest - @pytest.mark.hello - class TestClass(object): - pytestmark = pytest.mark.world - def test_func(self): - assert TestClass.test_func.hello - assert TestClass.test_func.world - """ - ) - keywords = item.keywords - assert "hello" in keywords - assert "world" in keywords - - @ignore_markinfo - def test_merging_markers(self, testdir): - p = testdir.makepyfile( - """ - import pytest - pytestmark = pytest.mark.hello("pos1", x=1, y=2) - class TestClass(object): - # classlevel overrides module level - pytestmark = pytest.mark.hello(x=3) - @pytest.mark.hello("pos0", z=4) - def test_func(self): - pass - """ - ) - items, rec = testdir.inline_genitems(p) - item, = items - keywords = item.keywords - marker = keywords["hello"] - assert marker.args == ("pos0", "pos1") - assert marker.kwargs == {"x": 1, "y": 2, "z": 4} - - # test the new __iter__ interface - values = list(marker) - assert len(values) == 3 - assert values[0].args == ("pos0",) - assert values[1].args == () - assert values[2].args == ("pos1",) - def test_merging_markers_deep(self, testdir): # issue 199 - propagate markers into nested classes p = testdir.makepyfile( @@ -675,11 +463,6 @@ class TestFunctional(object): items, rec = testdir.inline_genitems(p) base_item, sub_item, sub_item_other = items print(items, [x.nodeid for x in items]) - # legacy api smears - assert hasattr(base_item.obj, "b") - assert hasattr(sub_item_other.obj, "b") - assert hasattr(sub_item.obj, "b") - # new api seregates assert not list(base_item.iter_markers(name="b")) assert not list(sub_item_other.iter_markers(name="b")) @@ -765,26 +548,6 @@ class TestFunctional(object): result = testdir.runpytest() result.stdout.fnmatch_lines(["keyword: *hello*"]) - @ignore_markinfo - def test_merging_markers_two_functions(self, testdir): - p = testdir.makepyfile( - """ - import pytest - @pytest.mark.hello("pos1", z=4) - @pytest.mark.hello("pos0", z=3) - def test_func(): - pass - """ - ) - items, rec = testdir.inline_genitems(p) - item, = items - keywords = item.keywords - marker = keywords["hello"] - values = list(marker) - assert len(values) == 2 - assert values[0].args == ("pos0",) - assert values[1].args == ("pos1",) - def test_no_marker_match_on_unmarked_names(self, testdir): p = testdir.makepyfile( """ @@ -858,7 +621,7 @@ class TestFunctional(object): assert "mark2" in request.keywords assert "mark3" in request.keywords assert 10 not in request.keywords - marker = request.node.get_marker("mark1") + marker = request.node.get_closest_marker("mark1") assert marker.name == "mark1" assert marker.args == () assert marker.kwargs == {} @@ -874,15 +637,11 @@ class TestFunctional(object): .. note:: this could be moved to ``testdir`` if proven to be useful to other modules. """ - from _pytest.mark import MarkInfo items = {x.name: x for x in items} for name, expected_markers in expected.items(): - markers = items[name].keywords._markers - marker_names = { - name for (name, v) in markers.items() if isinstance(v, MarkInfo) - } - assert marker_names == set(expected_markers) + markers = {m.name for m in items[name].iter_markers()} + assert markers == set(expected_markers) @pytest.mark.issue1540 @pytest.mark.filterwarnings("ignore") @@ -1041,56 +800,6 @@ class TestKeywordSelection(object): assert_test_is_not_selected("()") -@pytest.mark.parametrize( - "argval, expected", - [ - ( - pytest.mark.skip()((1, 2)), - ParameterSet(values=(1, 2), marks=[pytest.mark.skip], id=None), - ), - ( - pytest.mark.xfail(pytest.mark.skip()((1, 2))), - ParameterSet( - values=(1, 2), marks=[pytest.mark.xfail, pytest.mark.skip], id=None - ), - ), - ], -) -@pytest.mark.filterwarnings("default") -def test_parameterset_extractfrom(argval, expected): - from _pytest.deprecated import MARK_PARAMETERSET_UNPACKING - - warn_called = [] - - class DummyItem: - def warn(self, warning): - warn_called.append(warning) - - extracted = ParameterSet.extract_from(argval, belonging_definition=DummyItem()) - assert extracted == expected - assert warn_called == [MARK_PARAMETERSET_UNPACKING] - - -def test_legacy_transfer(): - class FakeModule(object): - pytestmark = [] - - class FakeClass(object): - pytestmark = pytest.mark.nofun - - @pytest.mark.fun - def fake_method(self): - pass - - transfer_markers(fake_method, FakeClass, FakeModule) - - # legacy marks transfer smeared - assert fake_method.nofun - assert fake_method.fun - # pristine marks dont transfer - assert fake_method.pytestmark == [pytest.mark.fun.mark] - - class TestMarkDecorator(object): @pytest.mark.parametrize( "lhs, rhs, expected", @@ -1191,19 +900,12 @@ def test_mark_expressions_no_smear(testdir): deselected_tests = dlist[0].items assert len(deselected_tests) == 1 + # todo: fixed # keywords smear - expected behaviour - reprec_keywords = testdir.inline_run("-k", "FOO") - passed_k, skipped_k, failed_k = reprec_keywords.countoutcomes() - assert passed_k == 2 - assert skipped_k == failed_k == 0 - - -def test_addmarker_getmarker(): - node = Node("Test", config=mock.Mock(), session=mock.Mock(), nodeid="Test") - node.add_marker(pytest.mark.a(1)) - node.add_marker("b") - node.get_marker("a").combined - node.get_marker("b").combined + # reprec_keywords = testdir.inline_run("-k", "FOO") + # passed_k, skipped_k, failed_k = reprec_keywords.countoutcomes() + # assert passed_k == 2 + # assert skipped_k == failed_k == 0 def test_addmarker_order(): @@ -1227,7 +929,7 @@ def test_markers_from_parametrize(testdir): custom_mark = pytest.mark.custom_mark @pytest.fixture(autouse=True) def trigger(request): - custom_mark =request.node.get_marker('custom_mark') + custom_mark = list(request.node.iter_markers('custom_mark')) print("Custom mark %s" % custom_mark) @custom_mark("custom mark non parametrized") @@ -1252,3 +954,18 @@ def test_markers_from_parametrize(testdir): result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) result.assert_outcomes(passed=4) + + +def test_pytest_param_id_requires_string(): + with pytest.raises(TypeError) as excinfo: + pytest.param(id=True) + msg, = excinfo.value.args + if six.PY2: + assert msg == "Expected id to be a string, got : True" + else: + assert msg == "Expected id to be a string, got : True" + + +@pytest.mark.parametrize("s", (None, "hello world")) +def test_pytest_param_id_allows_none_or_string(s): + assert pytest.param(id=s) diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index ebc233fbf..9e44b4975 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -27,7 +27,7 @@ def test_setattr(): x = 1 monkeypatch = MonkeyPatch() - pytest.raises(AttributeError, "monkeypatch.setattr(A, 'notexists', 2)") + pytest.raises(AttributeError, monkeypatch.setattr, A, "notexists", 2) monkeypatch.setattr(A, "y", 2, raising=False) assert A.y == 2 monkeypatch.undo() @@ -99,7 +99,7 @@ def test_delattr(): monkeypatch = MonkeyPatch() monkeypatch.delattr(A, "x") - pytest.raises(AttributeError, "monkeypatch.delattr(A, 'y')") + pytest.raises(AttributeError, monkeypatch.delattr, A, "y") monkeypatch.delattr(A, "y", raising=False) monkeypatch.setattr(A, "x", 5, raising=False) assert A.x == 5 @@ -156,7 +156,7 @@ def test_delitem(): monkeypatch.delitem(d, "x") assert "x" not in d monkeypatch.delitem(d, "y", raising=False) - pytest.raises(KeyError, "monkeypatch.delitem(d, 'y')") + pytest.raises(KeyError, monkeypatch.delitem, d, "y") assert not d monkeypatch.setitem(d, "y", 1700) assert d["y"] == 1700 @@ -182,7 +182,7 @@ def test_delenv(): name = "xyz1234" assert name not in os.environ monkeypatch = MonkeyPatch() - pytest.raises(KeyError, "monkeypatch.delenv(%r, raising=True)" % name) + pytest.raises(KeyError, monkeypatch.delenv, name, raising=True) monkeypatch.delenv(name, raising=False) monkeypatch.undo() os.environ[name] = "1" diff --git a/testing/test_nose.py b/testing/test_nose.py index e4db46802..3e9966529 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -3,7 +3,6 @@ from __future__ import division from __future__ import print_function import pytest -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG def setup_module(mod): @@ -162,73 +161,6 @@ def test_nose_setup_partial(testdir): result.stdout.fnmatch_lines(["*2 passed*"]) -def test_nose_test_generator_fixtures(testdir): - p = testdir.makepyfile( - """ - # taken from nose-0.11.1 unit_tests/test_generator_fixtures.py - from nose.tools import eq_ - called = [] - - def outer_setup(): - called.append('outer_setup') - - def outer_teardown(): - called.append('outer_teardown') - - def inner_setup(): - called.append('inner_setup') - - def inner_teardown(): - called.append('inner_teardown') - - def test_gen(): - called[:] = [] - for i in range(0, 5): - yield check, i - - def check(i): - expect = ['outer_setup'] - for x in range(0, i): - expect.append('inner_setup') - expect.append('inner_teardown') - expect.append('inner_setup') - eq_(called, expect) - - - test_gen.setup = outer_setup - test_gen.teardown = outer_teardown - check.setup = inner_setup - check.teardown = inner_teardown - - class TestClass(object): - def setup(self): - print("setup called in %s" % self) - self.called = ['setup'] - - def teardown(self): - print("teardown called in %s" % self) - eq_(self.called, ['setup']) - self.called.append('teardown') - - def test(self): - print("test called in %s" % self) - for i in range(0, 5): - yield self.check, i - - def check(self, i): - print("check called in %s" % self) - expect = ['setup'] - #for x in range(0, i): - # expect.append('setup') - # expect.append('teardown') - #expect.append('setup') - eq_(self.called, expect) - """ - ) - result = testdir.runpytest(p, "-p", "nose", SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines(["*10 passed*"]) - - def test_module_level_setup(testdir): testdir.makepyfile( """ diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 0dafa248b..c3b4ee698 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -24,6 +24,12 @@ class TestParser(object): out, err = capsys.readouterr() assert err.find("error: unrecognized arguments") != -1 + def test_custom_prog(self, parser): + """Custom prog can be set for `argparse.ArgumentParser`.""" + assert parser._getparser().prog == os.path.basename(sys.argv[0]) + parser.prog = "custom-prog" + assert parser._getparser().prog == "custom-prog" + def test_argument(self): with pytest.raises(parseopt.ArgumentError): # need a short or long option @@ -100,12 +106,8 @@ class TestParser(object): def test_group_shortopt_lowercase(self, parser): group = parser.getgroup("hello") - pytest.raises( - ValueError, - """ + with pytest.raises(ValueError): group.addoption("-x", action="store_true") - """, - ) assert len(group.options) == 0 group._addoption("-x", action="store_true") assert len(group.options) == 1 diff --git a/testing/test_pdb.py b/testing/test_pdb.py index dd349454b..cb1017ac4 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -8,7 +8,6 @@ import sys import _pytest._code import pytest -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG try: breakpoint @@ -390,6 +389,28 @@ class TestPDB(object): assert "hello17" in rest # out is captured self.flush(child) + def test_pdb_set_trace_kwargs(self, testdir): + p1 = testdir.makepyfile( + """ + import pytest + def test_1(): + i = 0 + print("hello17") + pytest.set_trace(header="== my_header ==") + x = 3 + """ + ) + child = testdir.spawn_pytest(str(p1)) + child.expect("== my_header ==") + assert "PDB set_trace" not in child.before.decode() + child.expect("Pdb") + child.sendeof() + rest = child.read().decode("utf-8") + assert "1 failed" in rest + assert "def test_1" in rest + assert "hello17" in rest # out is captured + self.flush(child) + def test_pdb_set_trace_interception(self, testdir): p1 = testdir.makepyfile( """ @@ -634,6 +655,12 @@ class TestPDB(object): testdir.makepyfile( custom_pdb=""" class CustomPdb(object): + def __init__(self, *args, **kwargs): + skip = kwargs.pop("skip") + assert skip == ["foo.*"] + print("__init__") + super(CustomPdb, self).__init__(*args, **kwargs) + def set_trace(*args, **kwargs): print('custom set_trace>') """ @@ -643,12 +670,13 @@ class TestPDB(object): import pytest def test_foo(): - pytest.set_trace() + pytest.set_trace(skip=['foo.*']) """ ) monkeypatch.setenv("PYTHONPATH", str(testdir.tmpdir)) child = testdir.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1)) + child.expect("__init__") child.expect("custom set_trace>") self.flush(child) @@ -809,27 +837,6 @@ class TestTraceOption: assert "reading from stdin while output" not in rest TestPDB.flush(child) - def test_trace_against_yield_test(self, testdir): - p1 = testdir.makepyfile( - """ - def is_equal(a, b): - assert a == b - - def test_1(): - yield is_equal, 1, 1 - """ - ) - child = testdir.spawn_pytest( - "{} --trace {}".format(SHOW_PYTEST_WARNINGS_ARG, str(p1)) - ) - child.expect("is_equal") - child.expect("Pdb") - child.sendeof() - rest = child.read().decode("utf8") - assert "1 passed" in rest - assert "reading from stdin while output" not in rest - TestPDB.flush(child) - def test_trace_after_runpytest(testdir): """Test that debugging's pytest_configure is re-entrant.""" diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 8e35290b7..80817932e 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -32,7 +32,7 @@ class TestPytestPluginInteractions(object): """ import newhooks def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(newhooks) + pluginmanager.add_hookspecs(newhooks) def pytest_myhook(xyz): return xyz + 1 """ @@ -52,44 +52,13 @@ class TestPytestPluginInteractions(object): """ import sys def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(sys) + pluginmanager.add_hookspecs(sys) """ ) res = testdir.runpytest() assert res.ret != 0 res.stderr.fnmatch_lines(["*did not find*sys*"]) - def test_namespace_early_from_import(self, testdir): - p = testdir.makepyfile( - """ - from pytest import Item - from pytest import Item as Item2 - assert Item is Item2 - """ - ) - result = testdir.runpython(p) - assert result.ret == 0 - - @pytest.mark.filterwarnings("ignore:pytest_namespace is deprecated") - def test_do_ext_namespace(self, testdir): - testdir.makeconftest( - """ - def pytest_namespace(): - return {'hello': 'world'} - """ - ) - p = testdir.makepyfile( - """ - from pytest import hello - import pytest - def test_hello(): - assert hello == "world" - assert 'hello' in pytest.__all__ - """ - ) - reprec = testdir.inline_run(p) - reprec.assertoutcome(passed=1) - def test_do_option_postinitialize(self, testdir): config = testdir.parseconfigure() assert not hasattr(config.option, "test123") @@ -172,34 +141,6 @@ class TestPytestPluginInteractions(object): ihook_b = session.gethookproxy(testdir.tmpdir.join("tests")) assert ihook_a is not ihook_b - def test_warn_on_deprecated_addhooks(self, pytestpm): - warnings = [] - - class get_warnings(object): - def pytest_logwarning(self, code, fslocation, message, nodeid): - warnings.append(message) - - class Plugin(object): - def pytest_testhook(): - pass - - pytestpm.register(get_warnings()) - before = list(warnings) - pytestpm.addhooks(Plugin()) - assert len(warnings) == len(before) + 1 - assert "deprecated" in warnings[-1] - - -def test_namespace_has_default_and_env_plugins(testdir): - p = testdir.makepyfile( - """ - import pytest - pytest.mark - """ - ) - result = testdir.runpython(p) - assert result.ret == 0 - def test_default_markers(testdir): result = testdir.runpytest("--markers") @@ -238,7 +179,7 @@ class TestPytestPluginManager(object): assert pm.is_registered(mod) values = pm.get_plugins() assert mod in values - pytest.raises(ValueError, "pm.register(mod)") + pytest.raises(ValueError, pm.register, mod) pytest.raises(ValueError, lambda: pm.register(mod)) # assert not pm.is_registered(mod2) assert pm.get_plugins() == values @@ -282,11 +223,12 @@ class TestPytestPluginManager(object): with pytest.raises(ImportError): pytestpm.consider_env() + @pytest.mark.filterwarnings("always") def test_plugin_skip(self, testdir, monkeypatch): p = testdir.makepyfile( skipping1=""" import pytest - pytest.skip("hello") + pytest.skip("hello", allow_module_level=True) """ ) p.copy(p.dirpath("skipping2.py")) @@ -326,8 +268,8 @@ class TestPytestPluginManager(object): result.stdout.fnmatch_lines(["*1 passed*"]) def test_import_plugin_importname(self, testdir, pytestpm): - pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') - pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwx.y")') + pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y") + pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwx.y") testdir.syspathinsert() pluginname = "pytest_hello" @@ -343,8 +285,8 @@ class TestPytestPluginManager(object): assert plugin2 is plugin1 def test_import_plugin_dotted_name(self, testdir, pytestpm): - pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")') - pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwex.y")') + pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y") + pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwex.y") testdir.syspathinsert() testdir.mkpydir("pkg").join("plug.py").write("x=3") @@ -365,6 +307,12 @@ class TestPytestPluginManagerBootstrapming(object): ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"]) ) + # Handles -p without space (#3532). + with pytest.raises(ImportError) as excinfo: + pytestpm.consider_preparse(["-phello123"]) + assert '"hello123"' in excinfo.value.args[0] + pytestpm.consider_preparse(["-pno:hello123"]) + def test_plugin_prevent_register(self, pytestpm): pytestpm.consider_preparse(["xyz", "-p", "no:abc"]) l1 = pytestpm.get_plugins() diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 0c28bc91b..d14fbd18e 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -71,7 +71,7 @@ def test_make_hook_recorder(testdir): recorder.unregister() recorder.clear() recorder.hook.pytest_runtest_logreport(report=rep) - pytest.raises(ValueError, "recorder.getfailures()") + pytest.raises(ValueError, recorder.getfailures) def test_parseconfig(testdir): @@ -168,13 +168,13 @@ def make_holder(): @pytest.mark.parametrize("holder", make_holder()) def test_hookrecorder_basic(holder): pm = PytestPluginManager() - pm.addhooks(holder) + pm.add_hookspecs(holder) rec = HookRecorder(pm) pm.hook.pytest_xyz(arg=123) call = rec.popcall("pytest_xyz") assert call.arg == 123 assert call._name == "pytest_xyz" - pytest.raises(pytest.fail.Exception, "rec.popcall('abc')") + pytest.raises(pytest.fail.Exception, rec.popcall, "abc") pm.hook.pytest_xyz_noarg() call = rec.popcall("pytest_xyz_noarg") assert call._name == "pytest_xyz_noarg" @@ -280,7 +280,7 @@ def test_assert_outcomes_after_pytest_error(testdir): testdir.makepyfile("def test_foo(): assert True") result = testdir.runpytest("--unexpected-argument") - with pytest.raises(ValueError, message="Pytest terminal report not found"): + with pytest.raises(ValueError, match="Pytest terminal report not found"): result.assert_outcomes(passed=0) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 223521a5e..9bf6a2ffb 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -7,6 +7,7 @@ import warnings import pytest from _pytest.recwarn import WarningsRecorder +from _pytest.warning_types import PytestDeprecationWarning def test_recwarn_stacklevel(recwarn): @@ -44,7 +45,7 @@ class TestWarningsRecorderChecker(object): rec.clear() assert len(rec.list) == 0 assert values is rec.list - pytest.raises(AssertionError, "rec.pop()") + pytest.raises(AssertionError, rec.pop) @pytest.mark.issue(4243) def test_warn_stacklevel(self): @@ -214,9 +215,17 @@ class TestWarns(object): source1 = "warnings.warn('w1', RuntimeWarning)" source2 = "warnings.warn('w2', RuntimeWarning)" source3 = "warnings.warn('w3', RuntimeWarning)" - pytest.warns(RuntimeWarning, source1) - pytest.raises(pytest.fail.Exception, lambda: pytest.warns(UserWarning, source2)) - pytest.warns(RuntimeWarning, source3) + with pytest.warns(PytestDeprecationWarning) as warninfo: # yo dawg + pytest.warns(RuntimeWarning, source1) + pytest.raises( + pytest.fail.Exception, lambda: pytest.warns(UserWarning, source2) + ) + pytest.warns(RuntimeWarning, source3) + assert len(warninfo) == 3 + for w in warninfo: + assert w.filename == __file__ + msg, = w.message.args + assert msg.startswith("warns(..., 'code(as_a_string)') is deprecated") def test_function(self): pytest.warns( diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index 36f584e57..cb7b0cd3c 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -151,7 +151,7 @@ class TestWithFunctionIntegration(object): try: raise ValueError except ValueError: - excinfo = _pytest._code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo.from_current() reslog = ResultLog(None, py.io.TextIO()) reslog.pytest_internalerror(excinfo.getrepr(style=style)) entry = reslog.logfile.getvalue() diff --git a/testing/test_runner.py b/testing/test_runner.py index c081920a5..91f7d2700 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -487,13 +487,13 @@ def test_report_extra_parameters(reporttype): def test_callinfo(): - ci = runner.CallInfo(lambda: 0, "123") + ci = runner.CallInfo.from_call(lambda: 0, "123") assert ci.when == "123" assert ci.result == 0 assert "result" in repr(ci) assert repr(ci) == "" - ci = runner.CallInfo(lambda: 0 / 0, "123") + ci = runner.CallInfo.from_call(lambda: 0 / 0, "123") assert ci.when == "123" assert not hasattr(ci, "result") assert repr(ci) == "" @@ -501,16 +501,6 @@ def test_callinfo(): assert "exc" in repr(ci) -def test_callinfo_repr_while_running(): - def repr_while_running(): - f = sys._getframe().f_back - assert "func" in f.f_locals - assert repr(f.f_locals["self"]) == "'>" - - ci = runner.CallInfo(repr_while_running, "when") - assert repr(ci) == "" - - # design question: do we want general hooks in python files? # then something like the following functional tests makes sense @@ -561,20 +551,16 @@ def test_outcomeexception_passes_except_Exception(): def test_pytest_exit(): - try: + with pytest.raises(pytest.exit.Exception) as excinfo: pytest.exit("hello") - except pytest.exit.Exception: - excinfo = _pytest._code.ExceptionInfo() - assert excinfo.errisinstance(KeyboardInterrupt) + assert excinfo.errisinstance(pytest.exit.Exception) def test_pytest_fail(): - try: + with pytest.raises(pytest.fail.Exception) as excinfo: pytest.fail("hello") - except pytest.fail.Exception: - excinfo = _pytest._code.ExceptionInfo() - s = excinfo.exconly(tryshort=True) - assert s.startswith("Failed") + s = excinfo.exconly(tryshort=True) + assert s.startswith("Failed") def test_pytest_exit_msg(testdir): @@ -683,7 +669,7 @@ def test_exception_printing_skip(): try: pytest.skip("hello") except pytest.skip.Exception: - excinfo = _pytest._code.ExceptionInfo() + excinfo = _pytest._code.ExceptionInfo.from_current() s = excinfo.exconly(tryshort=True) assert s.startswith("Skipped") @@ -704,21 +690,17 @@ def test_importorskip(monkeypatch): # check that importorskip reports the actual call # in this test the test_runner.py file assert path.purebasename == "test_runner" - pytest.raises(SyntaxError, "pytest.importorskip('x y z')") - pytest.raises(SyntaxError, "pytest.importorskip('x=y')") + pytest.raises(SyntaxError, pytest.importorskip, "x y z") + pytest.raises(SyntaxError, pytest.importorskip, "x=y") mod = types.ModuleType("hello123") mod.__version__ = "1.3" monkeypatch.setitem(sys.modules, "hello123", mod) - pytest.raises( - pytest.skip.Exception, - """ + with pytest.raises(pytest.skip.Exception): pytest.importorskip("hello123", minversion="1.3.1") - """, - ) mod2 = pytest.importorskip("hello123", minversion="1.3") assert mod2 == mod except pytest.skip.Exception: - print(_pytest._code.ExceptionInfo()) + print(_pytest._code.ExceptionInfo.from_current()) pytest.fail("spurious skip") @@ -734,13 +716,10 @@ def test_importorskip_dev_module(monkeypatch): monkeypatch.setitem(sys.modules, "mockmodule", mod) mod2 = pytest.importorskip("mockmodule", minversion="0.12.0") assert mod2 == mod - pytest.raises( - pytest.skip.Exception, - """ - pytest.importorskip('mockmodule1', minversion='0.14.0')""", - ) + with pytest.raises(pytest.skip.Exception): + pytest.importorskip("mockmodule1", minversion="0.14.0") except pytest.skip.Exception: - print(_pytest._code.ExceptionInfo()) + print(_pytest._code.ExceptionInfo.from_current()) pytest.fail("spurious skip") @@ -759,6 +738,22 @@ def test_importorskip_module_level(testdir): result.stdout.fnmatch_lines(["*collected 0 items / 1 skipped*"]) +def test_importorskip_custom_reason(testdir): + """make sure custom reasons are used""" + testdir.makepyfile( + """ + import pytest + foobarbaz = pytest.importorskip("foobarbaz2", reason="just because") + + def test_foo(): + pass + """ + ) + result = testdir.runpytest("-ra") + result.stdout.fnmatch_lines(["*just because*"]) + result.stdout.fnmatch_lines(["*collected 0 items / 1 skipped*"]) + + def test_pytest_cmdline_main(testdir): p = testdir.makepyfile( """ diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index b0844dc1c..6b5752b77 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -7,7 +7,6 @@ from __future__ import division from __future__ import print_function import pytest -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG def test_module_and_function_setup(testdir): @@ -170,64 +169,6 @@ def test_method_setup_failure_no_teardown(testdir): reprec.assertoutcome(failed=1, passed=1) -def test_method_generator_setup(testdir): - reprec = testdir.inline_runsource( - """ - class TestSetupTeardownOnInstance(object): - def setup_class(cls): - cls.classsetup = True - - def setup_method(self, method): - self.methsetup = method - - def test_generate(self): - assert self.classsetup - assert self.methsetup == self.test_generate - yield self.generated, 5 - yield self.generated, 2 - - def generated(self, value): - assert self.classsetup - assert self.methsetup == self.test_generate - assert value == 5 - """, - SHOW_PYTEST_WARNINGS_ARG, - ) - reprec.assertoutcome(passed=1, failed=1) - - -def test_func_generator_setup(testdir): - reprec = testdir.inline_runsource( - """ - import sys - - def setup_module(mod): - print("setup_module") - mod.x = [] - - def setup_function(fun): - print("setup_function") - x.append(1) - - def teardown_function(fun): - print("teardown_function") - x.pop() - - def test_one(): - assert x == [1] - def check(): - print("check") - sys.stderr.write("e\\n") - assert x == [1] - yield check - assert x == [1] - """, - SHOW_PYTEST_WARNINGS_ARG, - ) - rep = reprec.matchreport("test_one", names="pytest_runtest_logreport") - assert rep.passed - - def test_method_setup_uses_fresh_instances(testdir): reprec = testdir.inline_runsource( """ diff --git a/testing/test_session.py b/testing/test_session.py index 0dc98a703..d68fc9d41 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -4,7 +4,6 @@ from __future__ import print_function import pytest from _pytest.main import EXIT_NOTESTSCOLLECTED -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG class SessionTests(object): @@ -73,19 +72,6 @@ class SessionTests(object): print(out) pytest.fail("incorrect raises() output") - def test_generator_yields_None(self, testdir): - reprec = testdir.inline_runsource( - """ - def test_1(): - yield None - """, - SHOW_PYTEST_WARNINGS_ARG, - ) - failures = reprec.getfailedcollections() - out = failures[0].longrepr.reprcrash.message - i = out.find("TypeError") - assert i != -1 - def test_syntax_error_module(self, testdir): reprec = testdir.inline_runsource("this is really not python") values = reprec.getfailedcollections() @@ -243,12 +229,8 @@ class TestNewSession(SessionTests): def test_plugin_specify(testdir): - pytest.raises( - ImportError, - """ - testdir.parseconfig("-p", "nqweotexistent") - """, - ) + with pytest.raises(ImportError): + testdir.parseconfig("-p", "nqweotexistent") # pytest.raises(ImportError, # "config.do_configure(config)" # ) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 231c3b6aa..6b18011b6 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -875,11 +875,22 @@ def test_reportchars_all(testdir): pass def test_4(): pytest.skip("four") + @pytest.fixture + def fail(): + assert 0 + def test_5(fail): + pass """ ) result = testdir.runpytest("-ra") result.stdout.fnmatch_lines( - ["FAIL*test_1*", "SKIP*four*", "XFAIL*test_2*", "XPASS*test_3*"] + [ + "SKIP*four*", + "XFAIL*test_2*", + "XPASS*test_3*", + "ERROR*test_5*", + "FAIL*test_1*", + ] ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 019dd66f4..06345f88d 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -20,7 +20,6 @@ from _pytest.terminal import build_summary_stats_line from _pytest.terminal import getreportopt from _pytest.terminal import repr_pythonversion from _pytest.terminal import TerminalReporter -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"]) @@ -105,7 +104,8 @@ class TestTerminal(object): def test_internalerror(self, testdir, linecomp): modcol = testdir.getmodulecol("def test_one(): pass") rep = TerminalReporter(modcol.config, file=linecomp.stringio) - excinfo = pytest.raises(ValueError, "raise ValueError('hello')") + with pytest.raises(ValueError) as excinfo: + raise ValueError("hello") rep.pytest_internalerror(excinfo.getrepr()) linecomp.assert_contains_lines(["INTERNALERROR> *ValueError*hello*"]) @@ -263,7 +263,7 @@ class TestCollectonly(object): ) result = testdir.runpytest("--collect-only") result.stdout.fnmatch_lines( - ["", " "] + ["", " "] ) def test_collectonly_skipped_module(self, testdir): @@ -276,6 +276,18 @@ class TestCollectonly(object): result = testdir.runpytest("--collect-only", "-rs") result.stdout.fnmatch_lines(["*ERROR collecting*"]) + def test_collectonly_display_test_description(self, testdir): + testdir.makepyfile( + """ + def test_with_description(): + \""" This test has a description. + \""" + assert True + """ + ) + result = testdir.runpytest("--collect-only", "--verbose") + result.stdout.fnmatch_lines([" This test has a description."]) + def test_collectonly_failed_module(self, testdir): testdir.makepyfile("""raise ValueError(0)""") result = testdir.runpytest("--collect-only") @@ -307,11 +319,10 @@ class TestCollectonly(object): assert result.ret == 0 result.stdout.fnmatch_lines( [ - "*", - "* ", - "* ", - # "* ", - "* ", + "*", + "* ", + "* ", + "* ", ] ) @@ -540,7 +551,7 @@ class TestTerminalFunctional(object): result.stdout.fnmatch_lines(["test_passes.py ..*", "* 2 pass*"]) assert result.ret == 0 - def test_header_trailer_info(self, testdir): + def test_header_trailer_info(self, testdir, request): testdir.makepyfile( """ def test_passes(): @@ -564,7 +575,7 @@ class TestTerminalFunctional(object): "=* 1 passed*in *.[0-9][0-9] seconds *=", ] ) - if pytest.config.pluginmanager.list_plugin_distinfo(): + if request.config.pluginmanager.list_plugin_distinfo(): result.stdout.fnmatch_lines(["plugins: *"]) def test_showlocals(self, testdir): @@ -585,8 +596,9 @@ class TestTerminalFunctional(object): ] ) - def test_verbose_reporting(self, testdir, pytestconfig): - p1 = testdir.makepyfile( + @pytest.fixture + def verbose_testfile(self, testdir): + return testdir.makepyfile( """ import pytest def test_fail(): @@ -602,22 +614,32 @@ class TestTerminalFunctional(object): yield check, 0 """ ) - result = testdir.runpytest(p1, "-v", SHOW_PYTEST_WARNINGS_ARG) + + def test_verbose_reporting(self, verbose_testfile, testdir, pytestconfig): + + result = testdir.runpytest( + verbose_testfile, "-v", "-Walways::pytest.PytestWarning" + ) result.stdout.fnmatch_lines( [ "*test_verbose_reporting.py::test_fail *FAIL*", "*test_verbose_reporting.py::test_pass *PASS*", "*test_verbose_reporting.py::TestClass::test_skip *SKIP*", - "*test_verbose_reporting.py::test_gen*0* *FAIL*", + "*test_verbose_reporting.py::test_gen *xfail*", ] ) assert result.ret == 1 + def test_verbose_reporting_xdist(self, verbose_testfile, testdir, pytestconfig): if not pytestconfig.pluginmanager.get_plugin("xdist"): pytest.skip("xdist plugin not installed") - result = testdir.runpytest(p1, "-v", "-n 1", SHOW_PYTEST_WARNINGS_ARG) - result.stdout.fnmatch_lines(["*FAIL*test_verbose_reporting.py::test_fail*"]) + result = testdir.runpytest( + verbose_testfile, "-v", "-n 1", "-Walways::pytest.PytestWarning" + ) + result.stdout.fnmatch_lines( + ["*FAIL*test_verbose_reporting_xdist.py::test_fail*"] + ) assert result.ret == 1 def test_quiet_reporting(self, testdir): diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 53d9c71cd..3bac9a545 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -308,9 +308,9 @@ def test_filterwarnings_mark_registration(testdir): def test_warning_captured_hook(testdir): testdir.makeconftest( """ - from _pytest.warnings import _issue_config_warning + from _pytest.warnings import _issue_warning_captured def pytest_configure(config): - _issue_config_warning(UserWarning("config warning"), config, stacklevel=2) + _issue_warning_captured(UserWarning("config warning"), config.hook, stacklevel=2) """ ) testdir.makepyfile( @@ -623,3 +623,63 @@ def test_removed_in_pytest4_warning_as_error(testdir, change_default): else: assert change_default in ("ini", "cmdline") result.stdout.fnmatch_lines(["* 1 passed in *"]) + + +class TestAssertionWarnings: + @staticmethod + def assert_result_warns(result, msg): + result.stdout.fnmatch_lines(["*PytestWarning: %s*" % msg]) + + def test_tuple_warning(self, testdir): + testdir.makepyfile( + """ + def test_foo(): + assert (1,2) + """ + ) + result = testdir.runpytest() + self.assert_result_warns( + result, "assertion is always true, perhaps remove parentheses?" + ) + + @staticmethod + def create_file(testdir, return_none): + testdir.makepyfile( + """ + def foo(return_none): + if return_none: + return None + else: + return False + + def test_foo(): + assert foo({return_none}) + """.format( + return_none=return_none + ) + ) + + def test_none_function_warns(self, testdir): + self.create_file(testdir, True) + result = testdir.runpytest() + self.assert_result_warns( + result, 'asserting the value None, please use "assert is None"' + ) + + def test_assert_is_none_no_warn(self, testdir): + testdir.makepyfile( + """ + def foo(): + return None + + def test_foo(): + assert foo() is None + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*1 passed in*"]) + + def test_false_function_no_warn(self, testdir): + self.create_file(testdir, False) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*1 failed in*"]) diff --git a/tox.ini b/tox.ini index 711954720..3d5c2fd56 100644 --- a/tox.ini +++ b/tox.ini @@ -121,6 +121,7 @@ setenv= setenv = {[testenv:py27-pluggymaster]setenv} [testenv:docs] +basepython = python3 skipsdist = True usedevelop = True changedir = doc/en @@ -130,7 +131,7 @@ commands = sphinx-build -W -b html . _build [testenv:doctesting] -basepython = python +basepython = python3 skipsdist = True deps = PyYAML @@ -147,6 +148,7 @@ deps = sphinx PyYAML regendoc>=0.6.1 + dataclasses whitelist_externals = rm make