Merge remote-tracking branch 'upstream/features' into merge-features-into-master
This commit is contained in:
commit
ee0a306ee4
|
@ -20,7 +20,9 @@ repos:
|
|||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
exclude: _pytest/debugging.py
|
||||
language_version: python3
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v1.8.0
|
||||
hooks:
|
||||
|
@ -41,6 +43,6 @@ repos:
|
|||
- id: changelogs-rst
|
||||
name: changelog filenames
|
||||
language: fail
|
||||
entry: 'changelog files must be named ####.(feature|bugfix|doc|removal|vendor|trivial).rst'
|
||||
exclude: changelog/(\d+\.(feature|bugfix|doc|removal|vendor|trivial).rst|README.rst|_template.rst)
|
||||
entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst'
|
||||
exclude: changelog/(\d+\.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst)
|
||||
files: ^changelog/
|
||||
|
|
2
AUTHORS
2
AUTHORS
|
@ -14,6 +14,7 @@ Allan Feldman
|
|||
Anatoly Bubenkoff
|
||||
Anders Hovmöller
|
||||
Andras Tim
|
||||
Andrea Cimatoribus
|
||||
Andreas Zeidler
|
||||
Andrzej Ostrowski
|
||||
Andy Freeland
|
||||
|
@ -120,6 +121,7 @@ Katerina Koukiou
|
|||
Kevin Cox
|
||||
Kodi B. Arfer
|
||||
Kostis Anagnostopoulos
|
||||
Kyle Altendorf
|
||||
Lawrence Mitchell
|
||||
Lee Kamentsky
|
||||
Lev Maximov
|
||||
|
|
157
CHANGELOG.rst
157
CHANGELOG.rst
|
@ -18,6 +18,163 @@ with advance notice in the **Deprecations** section of releases.
|
|||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 3.9.1 (2018-10-16)
|
||||
=========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#4159 <https://github.com/pytest-dev/pytest/issues/4159>`_: For test-suites containing test classes, the information about the subclassed
|
||||
module is now output only if a higher verbosity level is specified (at least
|
||||
"-vv").
|
||||
|
||||
|
||||
pytest 3.9.0 (2018-10-15 - not published due to a release automation bug)
|
||||
=========================================================================
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#3616 <https://github.com/pytest-dev/pytest/issues/3616>`_: The following accesses have been documented as deprecated for years, but are now actually emitting deprecation warnings.
|
||||
|
||||
* Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances. Now
|
||||
users will this warning::
|
||||
|
||||
usage of Function.Module is deprecated, please use pytest.Module instead
|
||||
|
||||
Users should just ``import pytest`` and access those objects using the ``pytest`` module.
|
||||
|
||||
* ``request.cached_setup``, this was the precursor of the setup/teardown mechanism available to fixtures. You can
|
||||
consult `funcarg comparision section in the docs <https://docs.pytest.org/en/latest/funcarg_compare.html>`_.
|
||||
|
||||
* 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_collect_make_item`` 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.
|
||||
|
||||
* The warning that produces the message below has changed to ``RemovedInPytest4Warning``::
|
||||
|
||||
getfuncargvalue is deprecated, use getfixturevalue
|
||||
|
||||
|
||||
- `#3988 <https://github.com/pytest-dev/pytest/issues/3988>`_: Add a Deprecation warning for pytest.ensuretemp as it was deprecated since a while.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#2293 <https://github.com/pytest-dev/pytest/issues/2293>`_: Improve usage errors messages by hiding internal details which can be distracting and noisy.
|
||||
|
||||
This has the side effect that some error conditions that previously raised generic errors (such as
|
||||
``ValueError`` for unregistered marks) are now raising ``Failed`` exceptions.
|
||||
|
||||
|
||||
- `#3332 <https://github.com/pytest-dev/pytest/issues/3332>`_: Improve the error displayed when a ``conftest.py`` file could not be imported.
|
||||
|
||||
In order to implement this, a new ``chain`` parameter was added to ``ExceptionInfo.getrepr``
|
||||
to show or hide chained tracebacks in Python 3 (defaults to ``True``).
|
||||
|
||||
|
||||
- `#3849 <https://github.com/pytest-dev/pytest/issues/3849>`_: Add ``empty_parameter_set_mark=fail_at_collect`` ini option for raising an exception when parametrize collects an empty set.
|
||||
|
||||
|
||||
- `#3964 <https://github.com/pytest-dev/pytest/issues/3964>`_: Log messages generated in the collection phase are shown when
|
||||
live-logging is enabled and/or when they are logged to a file.
|
||||
|
||||
|
||||
- `#3985 <https://github.com/pytest-dev/pytest/issues/3985>`_: Introduce ``tmp_path`` as a fixture providing a Path object.
|
||||
|
||||
|
||||
- `#4013 <https://github.com/pytest-dev/pytest/issues/4013>`_: Deprecation warnings are now shown even if you customize the warnings filters yourself. In the previous version
|
||||
any customization would override pytest's filters and deprecation warnings would fall back to being hidden by default.
|
||||
|
||||
|
||||
- `#4073 <https://github.com/pytest-dev/pytest/issues/4073>`_: Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``.
|
||||
|
||||
|
||||
- `#4098 <https://github.com/pytest-dev/pytest/issues/4098>`_: Add returncode argument to pytest.exit() to exit pytest with a specific return code.
|
||||
|
||||
|
||||
- `#4102 <https://github.com/pytest-dev/pytest/issues/4102>`_: Reimplement ``pytest.deprecated_call`` using ``pytest.warns`` so it supports the ``match='...'`` keyword argument.
|
||||
|
||||
This has the side effect that ``pytest.deprecated_call`` now raises ``pytest.fail.Exception`` instead
|
||||
of ``AssertionError``.
|
||||
|
||||
|
||||
- `#4149 <https://github.com/pytest-dev/pytest/issues/4149>`_: Require setuptools>=30.3 and move most of the metadata to ``setup.cfg``.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#2535 <https://github.com/pytest-dev/pytest/issues/2535>`_: Improve error message when test functions of ``unittest.TestCase`` subclasses use a parametrized fixture.
|
||||
|
||||
|
||||
- `#3057 <https://github.com/pytest-dev/pytest/issues/3057>`_: ``request.fixturenames`` now correctly returns the name of fixtures created by ``request.getfixturevalue()``.
|
||||
|
||||
|
||||
- `#3946 <https://github.com/pytest-dev/pytest/issues/3946>`_: Warning filters passed as command line options using ``-W`` now take precedence over filters defined in ``ini``
|
||||
configuration files.
|
||||
|
||||
|
||||
- `#4066 <https://github.com/pytest-dev/pytest/issues/4066>`_: Fix source reindenting by using ``textwrap.dedent`` directly.
|
||||
|
||||
|
||||
- `#4102 <https://github.com/pytest-dev/pytest/issues/4102>`_: ``pytest.warn`` will capture previously-warned warnings in Python 2. Previously they were never raised.
|
||||
|
||||
|
||||
- `#4108 <https://github.com/pytest-dev/pytest/issues/4108>`_: Resolve symbolic links for args.
|
||||
|
||||
This fixes running ``pytest tests/test_foo.py::test_bar``, where ``tests``
|
||||
is a symlink to ``project/app/tests``:
|
||||
previously ``project/app/conftest.py`` would be ignored for fixtures then.
|
||||
|
||||
|
||||
- `#4132 <https://github.com/pytest-dev/pytest/issues/4132>`_: Fix duplicate printing of internal errors when using ``--pdb``.
|
||||
|
||||
|
||||
- `#4135 <https://github.com/pytest-dev/pytest/issues/4135>`_: pathlib based tmpdir cleanup now correctly handles symlinks in the folder.
|
||||
|
||||
|
||||
- `#4152 <https://github.com/pytest-dev/pytest/issues/4152>`_: Display the filename when encountering ``SyntaxWarning``.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3713 <https://github.com/pytest-dev/pytest/issues/3713>`_: Update usefixtures documentation to clarify that it can't be used with fixture functions.
|
||||
|
||||
|
||||
- `#4058 <https://github.com/pytest-dev/pytest/issues/4058>`_: Update fixture documentation to specify that a fixture can be invoked twice in the scope it's defined for.
|
||||
|
||||
|
||||
- `#4064 <https://github.com/pytest-dev/pytest/issues/4064>`_: According to unittest.rst, setUpModule and tearDownModule were not implemented, but it turns out they are. So updated the documentation for unittest.
|
||||
|
||||
|
||||
- `#4151 <https://github.com/pytest-dev/pytest/issues/4151>`_: Add tempir testing example to CONTRIBUTING.rst guide
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#2293 <https://github.com/pytest-dev/pytest/issues/2293>`_: The internal ``MarkerError`` exception has been removed.
|
||||
|
||||
|
||||
- `#3988 <https://github.com/pytest-dev/pytest/issues/3988>`_: Port the implementation of tmpdir to pathlib.
|
||||
|
||||
|
||||
- `#4063 <https://github.com/pytest-dev/pytest/issues/4063>`_: Exclude 0.00 second entries from ``--duration`` output unless ``-vv`` is passed on the command-line.
|
||||
|
||||
|
||||
- `#4093 <https://github.com/pytest-dev/pytest/issues/4093>`_: Fixed formatting of string literals in internal tests.
|
||||
|
||||
|
||||
pytest 3.8.2 (2018-10-02)
|
||||
=========================
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Improve error message when test functions of ``unittest.TestCase`` subclasses use a parametrized fixture.
|
|
@ -1 +0,0 @@
|
|||
``request.fixturenames`` now correctly returns the name of fixtures created by ``request.getfixturevalue()``.
|
|
@ -1 +0,0 @@
|
|||
Update usefixtures documentation to clarify that it can't be used with fixture functions.
|
|
@ -1 +0,0 @@
|
|||
Update fixture documentation to specify that a fixture can be invoked twice in the scope it's defined for.
|
|
@ -1 +0,0 @@
|
|||
Exclude 0.00 second entries from ``--duration`` output unless ``-vv`` is passed on the command-line.
|
|
@ -1 +0,0 @@
|
|||
According to unittest.rst, setUpModule and tearDownModule were not implemented, but it turns out they are. So updated the documentation for unittest.
|
|
@ -1 +0,0 @@
|
|||
Fix source reindenting by using ``textwrap.dedent`` directly.
|
|
@ -1 +0,0 @@
|
|||
Fixed formatting of string literals in internal tests.
|
|
@ -1 +0,0 @@
|
|||
Fix duplicate printing of internal errors when using ``--pdb``.
|
|
@ -1 +0,0 @@
|
|||
Add tempir testing example to CONTRIBUTING.rst guide
|
|
@ -1 +0,0 @@
|
|||
Display the filename when encountering ``SyntaxWarning``.
|
|
@ -14,7 +14,8 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
|
|||
* ``feature``: new user facing features, like new command-line options and new behavior.
|
||||
* ``bugfix``: fixes a reported bug.
|
||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||
* ``removal``: feature deprecation or removal.
|
||||
* ``deprecation``: feature deprecation.
|
||||
* ``removal``: feature removal.
|
||||
* ``vendor``: changes in packages vendored in pytest.
|
||||
* ``trivial``: fixing a small typo or internal change that might be noteworthy.
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ Release announcements
|
|||
:maxdepth: 2
|
||||
|
||||
|
||||
release-3.9.1
|
||||
release-3.9.0
|
||||
release-3.8.2
|
||||
release-3.8.1
|
||||
release-3.8.0
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
pytest-3.9.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.9.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:
|
||||
|
||||
* Andrea Cimatoribus
|
||||
* Ankit Goel
|
||||
* Anthony Sottile
|
||||
* Ben Eyal
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Jeffrey Rackauckas
|
||||
* Jose Carlos Menezes
|
||||
* Kyle Altendorf
|
||||
* Niklas JQ
|
||||
* Palash Chatterjee
|
||||
* Ronny Pfannschmidt
|
||||
* Thomas Hess
|
||||
* Thomas Hisch
|
||||
* Tomer Keren
|
||||
* Victor Maryama
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
|
@ -0,0 +1,20 @@
|
|||
pytest-3.9.1
|
||||
=======================================
|
||||
|
||||
pytest 3.9.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Ronny Pfannschmidt
|
||||
* Thomas Hisch
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
|
@ -104,7 +104,9 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
See http://docs.python.org/library/warnings.html for information
|
||||
on warning categories.
|
||||
tmpdir_factory
|
||||
Return a TempdirFactory instance for the test session.
|
||||
Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
|
||||
tmp_path_factory
|
||||
Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
|
||||
tmpdir
|
||||
Return a temporary directory path object
|
||||
which is unique to each test function invocation,
|
||||
|
@ -113,6 +115,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
|||
path object.
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
tmp_path
|
||||
Return a temporary directory path object
|
||||
which is unique to each test function invocation,
|
||||
created as a sub directory of the base temporary
|
||||
directory. The returned object is a :class:`pathlib.Path`
|
||||
object.
|
||||
|
||||
.. note::
|
||||
|
||||
in python < 3.6 this is a pathlib2.Path
|
||||
|
||||
no tests ran in 0.12 seconds
|
||||
|
||||
|
|
|
@ -14,6 +14,67 @@ 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 <warnings>`.
|
||||
|
||||
Internal classes accessed through ``Node``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.9
|
||||
|
||||
Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances now issue
|
||||
this warning::
|
||||
|
||||
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.
|
||||
|
||||
``cached_setup``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.9
|
||||
|
||||
``request.cached_setup`` was the precursor of the setup/teardown mechanism available to fixtures.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
return request.cached_setup(
|
||||
setup=Session.create, teardown=lambda session: session.close(), scope="module"
|
||||
)
|
||||
|
||||
This should be updated to make use of standard fixture mechanisms:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def db_session():
|
||||
session = Session.create()
|
||||
yield session
|
||||
session.close()
|
||||
|
||||
|
||||
You can consult `funcarg comparision section in the docs <https://docs.pytest.org/en/latest/funcarg_compare.html>`_ for
|
||||
more information.
|
||||
|
||||
This has been documented as deprecated for years, but only now we are actually emitting deprecation warnings.
|
||||
|
||||
|
||||
Using ``Class`` in custom Collectors
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. 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_collect_make_item`` 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.
|
||||
|
||||
|
||||
``Config.warn`` and ``Node.warn``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -574,7 +574,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, tmpdir, tmpdir_factory
|
||||
> 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.
|
||||
|
||||
$REGENDOC_TMPDIR/b/test_error.py:1
|
||||
|
|
|
@ -175,3 +175,13 @@ Previous to version 2.4 to set a break point in code one needed to use ``pytest.
|
|||
This is no longer needed and one can use the native ``import pdb;pdb.set_trace()`` call directly.
|
||||
|
||||
For more details see :ref:`breakpoints`.
|
||||
|
||||
"compat" properties
|
||||
-------------------
|
||||
|
||||
.. deprecated:: 3.9
|
||||
|
||||
Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances have long
|
||||
been documented as deprecated, but started to emit warnings from pytest ``3.9`` and onward.
|
||||
|
||||
Users should just ``import pytest`` and access those objects using the ``pytest`` module.
|
||||
|
|
|
@ -982,6 +982,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
|||
|
||||
* ``skip`` skips tests with an empty parameterset (default)
|
||||
* ``xfail`` marks tests with an empty parameterset as xfail(run=False)
|
||||
* ``fail_at_collect`` raises an exception if parametrize collects an empty parameter set
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
|
|
|
@ -5,6 +5,76 @@
|
|||
Temporary directories and files
|
||||
================================================
|
||||
|
||||
The ``tmp_path`` fixture
|
||||
------------------------
|
||||
|
||||
.. versionadded:: 3.9
|
||||
|
||||
|
||||
You can use the ``tmpdir`` fixture which will
|
||||
provide a temporary directory unique to the test invocation,
|
||||
created in the `base temporary directory`_.
|
||||
|
||||
``tmpdir`` is a ``pathlib/pathlib2.Path`` object. Here is an example test usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_tmp_path.py
|
||||
import os
|
||||
|
||||
CONTENT = u"content"
|
||||
|
||||
|
||||
def test_create_file(tmp_path):
|
||||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
assert 0
|
||||
|
||||
Running this would result in a passed test except for the last
|
||||
``assert 0`` line which we use to look at values::
|
||||
|
||||
$ pytest test_tmp_path.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 1 item
|
||||
|
||||
test_tmp_path.py F [100%]
|
||||
|
||||
================================= FAILURES =================================
|
||||
_____________________________ test_create_file _____________________________
|
||||
|
||||
tmp_path = PosixPath('PYTEST_TMPDIR/test_create_file0')
|
||||
|
||||
def test_create_file(tmp_path):
|
||||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
test_tmp_path.py:13: AssertionError
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
The ``tmp_path_factory`` fixture
|
||||
--------------------------------
|
||||
|
||||
.. versionadded:: 3.9
|
||||
|
||||
|
||||
The ``tmp_path_facotry`` is a session-scoped fixture which can be used
|
||||
to create arbitrary temporary directories from any other fixture or test.
|
||||
|
||||
its intended to replace ``tmpdir_factory`` and returns :class:`pathlib.Path` instances.
|
||||
|
||||
|
||||
The 'tmpdir' fixture
|
||||
--------------------
|
||||
|
||||
|
|
|
@ -101,22 +101,28 @@ DeprecationWarning and PendingDeprecationWarning
|
|||
------------------------------------------------
|
||||
|
||||
.. versionadded:: 3.8
|
||||
.. versionchanged:: 3.9
|
||||
|
||||
By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` if no other warning filters
|
||||
are configured.
|
||||
By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning``.
|
||||
|
||||
To disable showing ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings, you might define any warnings
|
||||
filter either in the command-line or in the ini file, or you can use:
|
||||
Sometimes it is useful to hide some specific deprecation warnings that happen in code that you have no control over
|
||||
(such as third-party libraries), in which case you might use the standard warning filters options (ini or marks).
|
||||
For example:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
ignore::PendingDeprecationWarning
|
||||
ignore:.*U.*mode is deprecated:DeprecationWarning
|
||||
|
||||
|
||||
.. note::
|
||||
This makes pytest more compliant with `PEP-0506 <https://www.python.org/dev/peps/pep-0565/#recommended-filter-settings-for-test-runners>`_ which suggests that those warnings should
|
||||
If warnings are configured at the interpreter level, using
|
||||
the `PYTHONWARNINGS <https://docs.python.org/3/using/cmdline.html#envvar-PYTHONWARNINGS>`_ environment variable or the
|
||||
``-W`` command-line option, pytest will not configure any filters by default.
|
||||
|
||||
.. note::
|
||||
This feature makes pytest more compliant with `PEP-0506 <https://www.python.org/dev/peps/pep-0565/#recommended-filter-settings-for-test-runners>`_ which suggests that those warnings should
|
||||
be shown by default by test runners, but pytest doesn't follow ``PEP-0506`` completely because resetting all
|
||||
warning filters like suggested in the PEP will break existing test suites that configure warning filters themselves
|
||||
by calling ``warnings.simplefilter`` (see issue `#2430 <https://github.com/pytest-dev/pytest/issues/2430>`_
|
||||
|
|
|
@ -420,9 +420,21 @@ additionally it is possible to copy examples for a example folder before running
|
|||
============================= warnings summary =============================
|
||||
$REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time
|
||||
testdir.copy_example("test_example.py")
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Class is deprecated, please use pytest.Class instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.File is deprecated, please use pytest.File instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Function is deprecated, please use pytest.Function instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Instance is deprecated, please use pytest.Instance instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Item is deprecated, please use pytest.Item instead
|
||||
return getattr(object, name, default)
|
||||
$PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Module is deprecated, please use pytest.Module instead
|
||||
return getattr(object, name, default)
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
=================== 2 passed, 1 warnings in 0.12 seconds ===================
|
||||
=================== 2 passed, 7 warnings in 0.12 seconds ===================
|
||||
|
||||
For more information about the result object that ``runpytest()`` returns, and
|
||||
the methods that it provides please check out the :py:class:`RunResult
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
[build-system]
|
||||
requires = [
|
||||
"setuptools",
|
||||
# sync with setup.py until we discard non-pep-517/518
|
||||
"setuptools>=30.3",
|
||||
"setuptools-scm",
|
||||
"wheel",
|
||||
]
|
||||
|
@ -15,7 +16,12 @@ template = "changelog/_template.rst"
|
|||
|
||||
[[tool.towncrier.type]]
|
||||
directory = "removal"
|
||||
name = "Deprecations and Removals"
|
||||
name = "Removals"
|
||||
showcontent = true
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
directory = "deprecation"
|
||||
name = "Deprecations"
|
||||
showcontent = true
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
|
|
54
setup.cfg
54
setup.cfg
|
@ -1,3 +1,55 @@
|
|||
[metadata]
|
||||
|
||||
name = pytest
|
||||
description = pytest: simple powerful testing with Python
|
||||
long_description = file: README.rst
|
||||
url = https://docs.pytest.org/en/latest/
|
||||
project_urls =
|
||||
Source=https://github.com/pytest-dev/pytest
|
||||
Tracker=https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||
|
||||
license = MIT license
|
||||
license_file = LICENSE
|
||||
keywords = test, unittest
|
||||
classifiers =
|
||||
Development Status :: 6 - Mature
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: POSIX
|
||||
Operating System :: Microsoft :: Windows
|
||||
Operating System :: MacOS :: MacOS X
|
||||
Topic :: Software Development :: Testing
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Utilities
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.4
|
||||
Programming Language :: Python :: 3.5
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
|
||||
[options]
|
||||
zip_safe = no
|
||||
packages =
|
||||
_pytest
|
||||
_pytest.assertion
|
||||
_pytest._code
|
||||
_pytest.mark
|
||||
_pytest.config
|
||||
|
||||
py_modules = pytest
|
||||
python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
|
||||
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
pytest=pytest:main
|
||||
py.test=pytest:main
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/en/
|
||||
build-dir = doc/build
|
||||
|
@ -13,8 +65,6 @@ universal = 1
|
|||
ignore =
|
||||
_pytest/_version.py
|
||||
|
||||
[metadata]
|
||||
license_file = LICENSE
|
||||
|
||||
[devpi:upload]
|
||||
formats = sdist.tgz,bdist_wheel
|
||||
|
|
120
setup.py
120
setup.py
|
@ -1,126 +1,34 @@
|
|||
import os
|
||||
import sys
|
||||
import setuptools
|
||||
import pkg_resources
|
||||
from setuptools import setup
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 6 - Mature",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Topic :: Software Development :: Testing",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: Utilities",
|
||||
] + [
|
||||
("Programming Language :: Python :: %s" % x)
|
||||
for x in "2 2.7 3 3.4 3.5 3.6 3.7".split()
|
||||
]
|
||||
|
||||
with open("README.rst") as fd:
|
||||
long_description = fd.read()
|
||||
|
||||
|
||||
def get_environment_marker_support_level():
|
||||
"""
|
||||
Tests how well setuptools supports PEP-426 environment marker.
|
||||
|
||||
The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2
|
||||
so we're using that), see: https://setuptools.readthedocs.io/en/latest/history.html#id350
|
||||
|
||||
The support is later enhanced to allow direct conditional inclusions inside install_requires,
|
||||
which is now recommended by setuptools. It first appeared in 36.2.0, went broken with 36.2.1, and
|
||||
again worked since 36.2.2, so we're using that. See:
|
||||
https://setuptools.readthedocs.io/en/latest/history.html#v36-2-2
|
||||
https://github.com/pypa/setuptools/issues/1099
|
||||
|
||||
References:
|
||||
|
||||
* https://wheel.readthedocs.io/en/latest/index.html#defining-conditional-dependencies
|
||||
* https://www.python.org/dev/peps/pep-0426/#environment-markers
|
||||
* https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies
|
||||
"""
|
||||
try:
|
||||
version = pkg_resources.parse_version(setuptools.__version__)
|
||||
if version >= pkg_resources.parse_version("36.2.2"):
|
||||
return 2
|
||||
if version >= pkg_resources.parse_version("0.7.2"):
|
||||
return 1
|
||||
except Exception as exc:
|
||||
sys.stderr.write("Could not test setuptool's version: %s\n" % exc)
|
||||
|
||||
# as of testing on 2018-05-26 fedora was on version 37* and debian was on version 33+
|
||||
# we should consider erroring on those
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
extras_require = {}
|
||||
install_requires = [
|
||||
"py>=1.5.0", # if py gets upgrade to >=1.6, remove _width_of_current_line in terminal.py
|
||||
# TODO: if py gets upgrade to >=1.6,
|
||||
# remove _width_of_current_line in terminal.py
|
||||
INSTALL_REQUIRES = [
|
||||
"py>=1.5.0",
|
||||
"six>=1.10.0",
|
||||
"setuptools",
|
||||
"attrs>=17.4.0",
|
||||
"more-itertools>=4.0.0",
|
||||
"atomicwrites>=1.0",
|
||||
'funcsigs;python_version<"3.0"',
|
||||
'pathlib2>=2.2.0;python_version<"3.6"',
|
||||
'colorama;sys_platform=="win32"',
|
||||
]
|
||||
|
||||
|
||||
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
|
||||
# used by tox.ini to test with pluggy master
|
||||
if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ:
|
||||
install_requires.append("pluggy>=0.7")
|
||||
environment_marker_support_level = get_environment_marker_support_level()
|
||||
if environment_marker_support_level >= 2:
|
||||
install_requires.append('funcsigs;python_version<"3.0"')
|
||||
install_requires.append('pathlib2>=2.2.0;python_version<"3.6"')
|
||||
install_requires.append('colorama;sys_platform=="win32"')
|
||||
elif environment_marker_support_level == 1:
|
||||
extras_require[':python_version<"3.0"'] = ["funcsigs"]
|
||||
extras_require[':python_version<"3.6"'] = ["pathlib2>=2.2.0"]
|
||||
extras_require[':sys_platform=="win32"'] = ["colorama"]
|
||||
else:
|
||||
if sys.platform == "win32":
|
||||
install_requires.append("colorama")
|
||||
if sys.version_info < (3, 0):
|
||||
install_requires.append("funcsigs")
|
||||
if sys.version_info < (3, 6):
|
||||
install_requires.append("pathlib2>=2.2.0")
|
||||
INSTALL_REQUIRES.append("pluggy>=0.7")
|
||||
|
||||
|
||||
def main():
|
||||
setup(
|
||||
name="pytest",
|
||||
description="pytest: simple powerful testing with Python",
|
||||
long_description=long_description,
|
||||
use_scm_version={"write_to": "src/_pytest/_version.py"},
|
||||
url="https://docs.pytest.org/en/latest/",
|
||||
project_urls={
|
||||
"Source": "https://github.com/pytest-dev/pytest",
|
||||
"Tracker": "https://github.com/pytest-dev/pytest/issues",
|
||||
},
|
||||
license="MIT license",
|
||||
platforms=["unix", "linux", "osx", "cygwin", "win32"],
|
||||
author=(
|
||||
"Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, "
|
||||
"Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others"
|
||||
),
|
||||
entry_points={"console_scripts": ["pytest=pytest:main", "py.test=pytest:main"]},
|
||||
classifiers=classifiers,
|
||||
keywords="test unittest",
|
||||
# the following should be enabled for release
|
||||
setup_requires=["setuptools-scm"],
|
||||
setup_requires=["setuptools-scm", "setuptools>=30.3"],
|
||||
package_dir={"": "src"},
|
||||
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
|
||||
install_requires=install_requires,
|
||||
extras_require=extras_require,
|
||||
packages=[
|
||||
"_pytest",
|
||||
"_pytest.assertion",
|
||||
"_pytest._code",
|
||||
"_pytest.mark",
|
||||
"_pytest.config",
|
||||
],
|
||||
py_modules=["pytest"],
|
||||
zip_safe=False,
|
||||
install_requires=INSTALL_REQUIRES,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ from .code import Code # noqa
|
|||
from .code import ExceptionInfo # noqa
|
||||
from .code import Frame # noqa
|
||||
from .code import Traceback # noqa
|
||||
from .code import filter_traceback # noqa
|
||||
from .code import getrawcode # noqa
|
||||
from .source import Source # noqa
|
||||
from .source import compile_ as compile # noqa
|
||||
|
|
|
@ -6,8 +6,10 @@ import traceback
|
|||
from inspect import CO_VARARGS, CO_VARKEYWORDS
|
||||
|
||||
import attr
|
||||
import pluggy
|
||||
import re
|
||||
from weakref import ref
|
||||
import _pytest
|
||||
from _pytest.compat import _PY2, _PY3, PY35, safe_str
|
||||
from six import text_type
|
||||
import py
|
||||
|
@ -451,13 +453,35 @@ class ExceptionInfo(object):
|
|||
tbfilter=True,
|
||||
funcargs=False,
|
||||
truncate_locals=True,
|
||||
chain=True,
|
||||
):
|
||||
""" return str()able representation of this exception info.
|
||||
showlocals: show locals per traceback entry
|
||||
style: long|short|no|native traceback style
|
||||
tbfilter: hide entries (where __tracebackhide__ is true)
|
||||
"""
|
||||
Return str()able representation of this exception info.
|
||||
|
||||
in case of style==native, tbfilter and showlocals is ignored.
|
||||
:param bool showlocals:
|
||||
Show locals per traceback entry.
|
||||
Ignored if ``style=="native"``.
|
||||
|
||||
:param str style: long|short|no|native traceback style
|
||||
|
||||
:param bool abspath:
|
||||
If paths should be changed to absolute or left unchanged.
|
||||
|
||||
:param bool tbfilter:
|
||||
Hide entries that contain a local variable ``__tracebackhide__==True``.
|
||||
Ignored if ``style=="native"``.
|
||||
|
||||
:param bool funcargs:
|
||||
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
|
||||
|
||||
:param bool truncate_locals:
|
||||
With ``showlocals==True``, make sure locals can be safely represented as strings.
|
||||
|
||||
:param bool chain: if chained exceptions in Python 3 should be shown.
|
||||
|
||||
.. versionchanged:: 3.9
|
||||
|
||||
Added the ``chain`` parameter.
|
||||
"""
|
||||
if style == "native":
|
||||
return ReprExceptionInfo(
|
||||
|
@ -476,6 +500,7 @@ class ExceptionInfo(object):
|
|||
tbfilter=tbfilter,
|
||||
funcargs=funcargs,
|
||||
truncate_locals=truncate_locals,
|
||||
chain=chain,
|
||||
)
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
|
@ -516,6 +541,7 @@ class FormattedExcinfo(object):
|
|||
tbfilter = attr.ib(default=True)
|
||||
funcargs = attr.ib(default=False)
|
||||
truncate_locals = attr.ib(default=True)
|
||||
chain = attr.ib(default=True)
|
||||
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
|
||||
|
||||
def _getindent(self, source):
|
||||
|
@ -735,7 +761,7 @@ class FormattedExcinfo(object):
|
|||
reprcrash = None
|
||||
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
if e.__cause__ is not None:
|
||||
if e.__cause__ is not None and self.chain:
|
||||
e = e.__cause__
|
||||
excinfo = (
|
||||
ExceptionInfo((type(e), e, e.__traceback__))
|
||||
|
@ -743,7 +769,11 @@ class FormattedExcinfo(object):
|
|||
else None
|
||||
)
|
||||
descr = "The above exception was the direct cause of the following exception:"
|
||||
elif e.__context__ is not None and not e.__suppress_context__:
|
||||
elif (
|
||||
e.__context__ is not None
|
||||
and not e.__suppress_context__
|
||||
and self.chain
|
||||
):
|
||||
e = e.__context__
|
||||
excinfo = (
|
||||
ExceptionInfo((type(e), e, e.__traceback__))
|
||||
|
@ -979,3 +1009,36 @@ else:
|
|||
return "maximum recursion depth exceeded" in str(excinfo.value)
|
||||
except UnicodeError:
|
||||
return False
|
||||
|
||||
|
||||
# relative paths that we use to filter traceback entries from appearing to the user;
|
||||
# see filter_traceback
|
||||
# note: if we need to add more paths than what we have now we should probably use a list
|
||||
# for better maintenance
|
||||
|
||||
_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||
# pluggy is either a package or a single module depending on the version
|
||||
if _PLUGGY_DIR.basename == "__init__.py":
|
||||
_PLUGGY_DIR = _PLUGGY_DIR.dirpath()
|
||||
_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath()
|
||||
_PY_DIR = py.path.local(py.__file__).dirpath()
|
||||
|
||||
|
||||
def filter_traceback(entry):
|
||||
"""Return True if a TracebackEntry instance should be removed from tracebacks:
|
||||
* dynamically generated code (no code to show up for it);
|
||||
* internal traceback from pytest or its internal libraries, py and pluggy.
|
||||
"""
|
||||
# entry.path might sometimes return a str object when the entry
|
||||
# points to dynamically generated code
|
||||
# see https://bitbucket.org/pytest-dev/py/issues/71
|
||||
raw_filename = entry.frame.code.raw.co_filename
|
||||
is_generated = "<" in raw_filename and ">" in raw_filename
|
||||
if is_generated:
|
||||
return False
|
||||
# entry.path might point to a non-existing file, in which case it will
|
||||
# also return a str object. see #1133
|
||||
p = py.path.local(entry.path)
|
||||
return (
|
||||
not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR)
|
||||
)
|
||||
|
|
|
@ -17,8 +17,9 @@ import atomicwrites
|
|||
import py
|
||||
|
||||
from _pytest.assertion import util
|
||||
from _pytest.compat import PurePath, spec_from_file_location
|
||||
from _pytest.paths import fnmatch_ex
|
||||
from _pytest.pathlib import PurePath
|
||||
from _pytest.compat import spec_from_file_location
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
|
||||
# pytest caches rewritten pycs in __pycache__.
|
||||
if hasattr(imp, "get_tag"):
|
||||
|
|
|
@ -13,10 +13,9 @@ import attr
|
|||
|
||||
import pytest
|
||||
import json
|
||||
import shutil
|
||||
|
||||
from . import paths
|
||||
from .compat import _PY2 as PY2, Path
|
||||
from .compat import _PY2 as PY2
|
||||
from .pathlib import Path, resolve_from_str, rmtree
|
||||
|
||||
README_CONTENT = u"""\
|
||||
# pytest cache directory #
|
||||
|
@ -39,13 +38,13 @@ class Cache(object):
|
|||
def for_config(cls, config):
|
||||
cachedir = cls.cache_dir_from_config(config)
|
||||
if config.getoption("cacheclear") and cachedir.exists():
|
||||
shutil.rmtree(str(cachedir))
|
||||
rmtree(cachedir, force=True)
|
||||
cachedir.mkdir()
|
||||
return cls(cachedir, config)
|
||||
|
||||
@staticmethod
|
||||
def cache_dir_from_config(config):
|
||||
return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir)
|
||||
return resolve_from_str(config.getini("cache_dir"), config.rootdir)
|
||||
|
||||
def warn(self, fmt, **args):
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
|
|
@ -23,8 +23,6 @@ except ImportError: # pragma: no cover
|
|||
# Only available in Python 3.4+ or as a backport
|
||||
enum = None
|
||||
|
||||
__all__ = ["Path", "PurePath"]
|
||||
|
||||
_PY3 = sys.version_info > (3, 0)
|
||||
_PY2 = not _PY3
|
||||
|
||||
|
@ -41,11 +39,6 @@ PY35 = sys.version_info[:2] >= (3, 5)
|
|||
PY36 = sys.version_info[:2] >= (3, 6)
|
||||
MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError"
|
||||
|
||||
if PY36:
|
||||
from pathlib import Path, PurePath
|
||||
else:
|
||||
from pathlib2 import Path, PurePath
|
||||
|
||||
|
||||
if _PY3:
|
||||
from collections.abc import MutableMapping as MappingMixin
|
||||
|
|
|
@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function
|
|||
import argparse
|
||||
import inspect
|
||||
import shlex
|
||||
import traceback
|
||||
import types
|
||||
import warnings
|
||||
import copy
|
||||
|
@ -19,6 +18,7 @@ import _pytest._code
|
|||
import _pytest.hookspec # the extension point definitions
|
||||
import _pytest.assertion
|
||||
from pluggy import PluginManager, HookimplMarker, HookspecMarker
|
||||
from _pytest._code import ExceptionInfo, filter_traceback
|
||||
from _pytest.compat import safe_str
|
||||
from .exceptions import UsageError, PrintHelp
|
||||
from .findpaths import determine_setup, exists
|
||||
|
@ -26,9 +26,6 @@ from .findpaths import determine_setup, exists
|
|||
hookimpl = HookimplMarker("pytest")
|
||||
hookspec = HookspecMarker("pytest")
|
||||
|
||||
# pytest startup
|
||||
#
|
||||
|
||||
|
||||
class ConftestImportFailure(Exception):
|
||||
def __init__(self, path, excinfo):
|
||||
|
@ -36,12 +33,6 @@ class ConftestImportFailure(Exception):
|
|||
self.path = path
|
||||
self.excinfo = excinfo
|
||||
|
||||
def __str__(self):
|
||||
etype, evalue, etb = self.excinfo
|
||||
formatted = traceback.format_tb(etb)
|
||||
# The level of the tracebacks we want to print is hand crafted :(
|
||||
return repr(evalue) + "\n" + "".join(formatted[2:])
|
||||
|
||||
|
||||
def main(args=None, plugins=None):
|
||||
""" return exit code, after performing an in-process test run.
|
||||
|
@ -57,10 +48,20 @@ def main(args=None, plugins=None):
|
|||
try:
|
||||
config = _prepareconfig(args, plugins)
|
||||
except ConftestImportFailure as e:
|
||||
exc_info = ExceptionInfo(e.excinfo)
|
||||
tw = py.io.TerminalWriter(sys.stderr)
|
||||
for line in traceback.format_exception(*e.excinfo):
|
||||
tw.line(
|
||||
"ImportError while loading conftest '{e.path}'.".format(e=e), red=True
|
||||
)
|
||||
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
|
||||
exc_repr = (
|
||||
exc_info.getrepr(style="short", chain=False)
|
||||
if exc_info.traceback
|
||||
else exc_info.exconly()
|
||||
)
|
||||
formatted_tb = safe_str(exc_repr)
|
||||
for line in formatted_tb.splitlines():
|
||||
tw.line(line.rstrip(), red=True)
|
||||
tw.line("ERROR: could not load %s\n" % (e.path,), red=True)
|
||||
return 4
|
||||
else:
|
||||
try:
|
||||
|
@ -378,17 +379,19 @@ class PytestPluginManager(PluginManager):
|
|||
def _getconftestmodules(self, path):
|
||||
if self._noconftest:
|
||||
return []
|
||||
try:
|
||||
return self._path2confmods[path]
|
||||
except KeyError:
|
||||
|
||||
if path.isfile():
|
||||
clist = self._getconftestmodules(path.dirpath())
|
||||
directory = path.dirpath()
|
||||
else:
|
||||
directory = path
|
||||
try:
|
||||
return self._path2confmods[directory]
|
||||
except KeyError:
|
||||
# XXX these days we may rather want to use config.rootdir
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
# directories instead of requiring to specify confcutdir
|
||||
clist = []
|
||||
for parent in path.parts():
|
||||
for parent in directory.parts():
|
||||
if self._confcutdir and self._confcutdir.relto(parent):
|
||||
continue
|
||||
conftestpath = parent.join("conftest.py")
|
||||
|
@ -396,7 +399,7 @@ class PytestPluginManager(PluginManager):
|
|||
mod = self._importconftest(conftestpath)
|
||||
clist.append(mod)
|
||||
|
||||
self._path2confmods[path] = clist
|
||||
self._path2confmods[directory] = clist
|
||||
return clist
|
||||
|
||||
def _rget_with_confmod(self, name, path):
|
||||
|
|
|
@ -103,21 +103,18 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None):
|
|||
if inifile:
|
||||
iniconfig = py.iniconfig.IniConfig(inifile)
|
||||
is_cfg_file = str(inifile).endswith(".cfg")
|
||||
# TODO: [pytest] section in *.cfg files is depricated. Need refactoring.
|
||||
sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"]
|
||||
for section in sections:
|
||||
try:
|
||||
inicfg = iniconfig[section]
|
||||
if is_cfg_file and section == "pytest" and config is not None:
|
||||
from _pytest.deprecated import CFG_PYTEST_SECTION
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
||||
# TODO: [pytest] section in *.cfg files is deprecated. Need refactoring once
|
||||
# the deprecation expires.
|
||||
_issue_config_warning(
|
||||
RemovedInPytest4Warning(
|
||||
CFG_PYTEST_SECTION.format(filename=str(inifile))
|
||||
),
|
||||
config,
|
||||
CFG_PYTEST_SECTION.format(filename=str(inifile)), config
|
||||
)
|
||||
break
|
||||
except KeyError:
|
||||
|
|
|
@ -4,10 +4,15 @@ that is planned to be removed in the next pytest release.
|
|||
|
||||
Keeping it in a central location makes it easy to track what is deprecated and should
|
||||
be removed when the time comes.
|
||||
|
||||
All constants defined in this module should be either PytestWarning instances or UnformattedWarning
|
||||
in case of warnings which need to format their messages.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
|
||||
from _pytest.warning_types import UnformattedWarning, RemovedInPytest4Warning
|
||||
|
||||
|
||||
MAIN_STR_ARGS = RemovedInPytest4Warning(
|
||||
"passing a string to pytest.main() is deprecated, "
|
||||
|
@ -18,25 +23,48 @@ YIELD_TESTS = RemovedInPytest4Warning(
|
|||
"yield tests are deprecated, and scheduled to be removed in pytest 4.0"
|
||||
)
|
||||
|
||||
FUNCARG_PREFIX = (
|
||||
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."
|
||||
"Please remove the prefix and use the @pytest.fixture decorator instead.",
|
||||
)
|
||||
|
||||
FIXTURE_FUNCTION_CALL = (
|
||||
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."
|
||||
"See https://docs.pytest.org/en/latest/fixture.html for more information.",
|
||||
)
|
||||
|
||||
CFG_PYTEST_SECTION = (
|
||||
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead."
|
||||
CFG_PYTEST_SECTION = UnformattedWarning(
|
||||
RemovedInPytest4Warning,
|
||||
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead.",
|
||||
)
|
||||
|
||||
GETFUNCARGVALUE = "getfuncargvalue is deprecated, use getfixturevalue"
|
||||
GETFUNCARGVALUE = RemovedInPytest4Warning(
|
||||
"getfuncargvalue is deprecated, use getfixturevalue"
|
||||
)
|
||||
|
||||
RESULT_LOG = (
|
||||
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."
|
||||
)
|
||||
|
@ -81,3 +109,8 @@ PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = RemovedInPytest4Warning(
|
|||
PYTEST_NAMESPACE = RemovedInPytest4Warning(
|
||||
"pytest_namespace is deprecated and will be removed soon"
|
||||
)
|
||||
|
||||
PYTEST_ENSURETEMP = RemovedInPytest4Warning(
|
||||
"pytest/tmpdir_factory.ensuretemp is deprecated, \n"
|
||||
"please use the tmp_path fixture or tmp_path_factory.mktemp"
|
||||
)
|
||||
|
|
|
@ -32,7 +32,7 @@ from _pytest.compat import (
|
|||
get_real_method,
|
||||
_PytestWrapper,
|
||||
)
|
||||
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
|
||||
from _pytest.deprecated import FIXTURE_FUNCTION_CALL
|
||||
from _pytest.outcomes import fail, TEST_OUTCOME
|
||||
|
||||
FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}'
|
||||
|
@ -481,6 +481,9 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
|||
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)
|
||||
|
@ -514,7 +517,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
|||
""" Deprecated, use getfixturevalue. """
|
||||
from _pytest import deprecated
|
||||
|
||||
warnings.warn(deprecated.GETFUNCARGVALUE, DeprecationWarning, stacklevel=2)
|
||||
warnings.warn(deprecated.GETFUNCARGVALUE, stacklevel=2)
|
||||
return self.getfixturevalue(argname)
|
||||
|
||||
def _get_active_fixturedef(self, argname):
|
||||
|
@ -576,7 +579,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
|||
nodeid=funcitem.nodeid,
|
||||
typename=type(funcitem).__name__,
|
||||
)
|
||||
fail(msg)
|
||||
fail(msg, pytrace=False)
|
||||
if has_params:
|
||||
frame = inspect.stack()[3]
|
||||
frameinfo = inspect.getframeinfo(frame[0])
|
||||
|
@ -597,7 +600,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
|||
source_lineno,
|
||||
)
|
||||
)
|
||||
fail(msg)
|
||||
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)
|
||||
|
@ -715,10 +718,11 @@ def scope2index(scope, descr, where=None):
|
|||
try:
|
||||
return scopes.index(scope)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"{} {}has an unsupported scope value '{}'".format(
|
||||
fail(
|
||||
"{} {}got an unexpected scope value '{}'".format(
|
||||
descr, "from {} ".format(where) if where else "", scope
|
||||
)
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
|
||||
|
@ -851,7 +855,9 @@ class FixtureDef(object):
|
|||
self.argname = argname
|
||||
self.scope = scope
|
||||
self.scopenum = scope2index(
|
||||
scope or "function", descr="fixture {}".format(func.__name__), where=baseid
|
||||
scope or "function",
|
||||
descr="Fixture '{}'".format(func.__name__),
|
||||
where=baseid,
|
||||
)
|
||||
self.params = params
|
||||
self.argnames = getfuncargnames(func, is_method=unittest)
|
||||
|
@ -973,8 +979,9 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker):
|
|||
used as an argument in a test function.
|
||||
"""
|
||||
is_yield_function = is_generator(function)
|
||||
msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__)
|
||||
warning = RemovedInPytest4Warning(msg)
|
||||
warning = FIXTURE_FUNCTION_CALL.format(
|
||||
name=fixture_marker.name or function.__name__
|
||||
)
|
||||
|
||||
if is_yield_function:
|
||||
|
||||
|
@ -1168,7 +1175,7 @@ class FixtureManager(object):
|
|||
def pytest_plugin_registered(self, plugin):
|
||||
nodeid = None
|
||||
try:
|
||||
p = py.path.local(plugin.__file__)
|
||||
p = py.path.local(plugin.__file__).realpath()
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
|
@ -1301,9 +1308,7 @@ class FixtureManager(object):
|
|||
|
||||
filename, lineno = getfslineno(obj)
|
||||
warnings.warn_explicit(
|
||||
RemovedInPytest4Warning(
|
||||
deprecated.FUNCARG_PREFIX.format(name=name)
|
||||
),
|
||||
deprecated.FUNCARG_PREFIX.format(name=name),
|
||||
category=None,
|
||||
filename=str(filename),
|
||||
lineno=lineno + 1,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import logging
|
||||
from contextlib import closing, contextmanager
|
||||
from contextlib import contextmanager
|
||||
import re
|
||||
import six
|
||||
|
||||
|
@ -415,7 +415,6 @@ class LoggingPlugin(object):
|
|||
else:
|
||||
self.log_file_handler = None
|
||||
|
||||
# initialized during pytest_runtestloop
|
||||
self.log_cli_handler = None
|
||||
|
||||
def _log_cli_enabled(self):
|
||||
|
@ -426,6 +425,22 @@ class LoggingPlugin(object):
|
|||
"--log-cli-level"
|
||||
) is not None or self._config.getini("log_cli")
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection(self):
|
||||
# This has to be called before the first log message is logged,
|
||||
# so we can access the terminal reporter plugin.
|
||||
self._setup_cli_logging()
|
||||
|
||||
with self.live_logs_context():
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when("collection")
|
||||
|
||||
if self.log_file_handler is not None:
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
@contextmanager
|
||||
def _runtest_for(self, item, when):
|
||||
"""Implements the internals of pytest_runtest_xxx() hook."""
|
||||
|
@ -485,22 +500,15 @@ class LoggingPlugin(object):
|
|||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtestloop(self, session):
|
||||
"""Runs all collected test items."""
|
||||
self._setup_cli_logging()
|
||||
with self.live_logs_context:
|
||||
with self.live_logs_context():
|
||||
if self.log_file_handler is not None:
|
||||
with closing(self.log_file_handler):
|
||||
with catching_logs(
|
||||
self.log_file_handler, level=self.log_file_level
|
||||
):
|
||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||
yield # run all the tests
|
||||
else:
|
||||
yield # run all the tests
|
||||
|
||||
def _setup_cli_logging(self):
|
||||
"""Sets up the handler and logger for the Live Logs feature, if enabled.
|
||||
|
||||
This must be done right before starting the loop so we can access the terminal reporter plugin.
|
||||
"""
|
||||
"""Sets up the handler and logger for the Live Logs feature, if enabled."""
|
||||
terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter")
|
||||
if self._log_cli_enabled() and terminal_reporter is not None:
|
||||
capture_manager = self._config.pluginmanager.get_plugin("capturemanager")
|
||||
|
@ -530,11 +538,14 @@ class LoggingPlugin(object):
|
|||
self._config, "log_cli_level", "log_level"
|
||||
)
|
||||
self.log_cli_handler = log_cli_handler
|
||||
self.live_logs_context = catching_logs(
|
||||
self.live_logs_context = lambda: catching_logs(
|
||||
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
|
||||
)
|
||||
else:
|
||||
self.live_logs_context = dummy_context_manager()
|
||||
self.live_logs_context = lambda: dummy_context_manager()
|
||||
# Note that the lambda for the live_logs_context is needed because
|
||||
# live_logs_context can otherwise not be entered multiple times due
|
||||
# to limitations of contextlib.contextmanager
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
|
|
|
@ -156,7 +156,10 @@ def pytest_addoption(parser):
|
|||
dest="basetemp",
|
||||
default=None,
|
||||
metavar="dir",
|
||||
help="base temporary directory for this test run.",
|
||||
help=(
|
||||
"base temporary directory for this test run."
|
||||
"(warning: this directory is removed if it exists)"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@ -182,10 +185,13 @@ def wrap_session(config, doit):
|
|||
session.exitstatus = EXIT_TESTSFAILED
|
||||
except KeyboardInterrupt:
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
if initstate < 2 and isinstance(excinfo.value, exit.Exception):
|
||||
exitstatus = EXIT_INTERRUPTED
|
||||
if initstate <= 2 and isinstance(excinfo.value, exit.Exception):
|
||||
sys.stderr.write("{}: {}\n".format(excinfo.typename, excinfo.value.msg))
|
||||
if excinfo.value.returncode is not None:
|
||||
exitstatus = excinfo.value.returncode
|
||||
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
|
||||
session.exitstatus = EXIT_INTERRUPTED
|
||||
session.exitstatus = exitstatus
|
||||
except: # noqa
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
config.notify_exception(excinfo, config.option)
|
||||
|
@ -487,7 +493,7 @@ class Session(nodes.FSCollector):
|
|||
from _pytest.python import Package
|
||||
|
||||
names = self._parsearg(arg)
|
||||
argpath = names.pop(0)
|
||||
argpath = names.pop(0).realpath()
|
||||
paths = []
|
||||
|
||||
root = self
|
||||
|
|
|
@ -24,11 +24,6 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
class MarkerError(Exception):
|
||||
|
||||
"""Error in use of a pytest marker/attribute."""
|
||||
|
||||
|
||||
def param(*values, **kw):
|
||||
"""Specify a parameter in `pytest.mark.parametrize`_ calls or
|
||||
:ref:`parametrized fixtures <fixture-parametrize-marks>`.
|
||||
|
@ -163,9 +158,9 @@ def pytest_configure(config):
|
|||
|
||||
empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
|
||||
if empty_parameterset not in ("skip", "xfail", None, ""):
|
||||
if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):
|
||||
raise UsageError(
|
||||
"{!s} must be one of skip and xfail,"
|
||||
"{!s} must be one of skip, xfail or fail_at_collect"
|
||||
" but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset)
|
||||
)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from operator import attrgetter
|
|||
|
||||
import attr
|
||||
|
||||
from _pytest.outcomes import fail
|
||||
from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE
|
||||
from ..compat import NOTSET, getfslineno, MappingMixin
|
||||
from six.moves import map
|
||||
|
@ -32,11 +33,19 @@ def istestfunc(func):
|
|||
|
||||
|
||||
def get_empty_parameterset_mark(config, argnames, func):
|
||||
from ..nodes import Collector
|
||||
|
||||
requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
if requested_mark in ("", None, "skip"):
|
||||
mark = MARK_GEN.skip
|
||||
elif requested_mark == "xfail":
|
||||
mark = MARK_GEN.xfail(run=False)
|
||||
elif requested_mark == "fail_at_collect":
|
||||
f_name = func.__name__
|
||||
_, lineno = getfslineno(func)
|
||||
raise Collector.CollectError(
|
||||
"Empty parameter set in '%s' at line %d" % (f_name, lineno)
|
||||
)
|
||||
else:
|
||||
raise LookupError(requested_mark)
|
||||
fs, lineno = getfslineno(func)
|
||||
|
@ -307,7 +316,7 @@ def _marked(func, mark):
|
|||
return any(mark == info.combined for info in func_mark)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(repr=False)
|
||||
class MarkInfo(object):
|
||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||
|
||||
|
@ -385,7 +394,7 @@ class MarkGenerator(object):
|
|||
x = marker.split("(", 1)[0]
|
||||
values.add(x)
|
||||
if name not in self._markers:
|
||||
raise AttributeError("%r not a registered marker" % (name,))
|
||||
fail("{!r} not a registered marker".format(name), pytrace=False)
|
||||
|
||||
|
||||
MARK_GEN = MarkGenerator()
|
||||
|
|
|
@ -9,6 +9,7 @@ import attr
|
|||
import _pytest
|
||||
import _pytest._code
|
||||
from _pytest.compat import getfslineno
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
from _pytest.mark.structures import NodeKeywords, MarkInfo
|
||||
|
||||
|
@ -61,11 +62,11 @@ class _CompatProperty(object):
|
|||
if obj is None:
|
||||
return self
|
||||
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn(
|
||||
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
|
||||
# name=self.name, owner=type(owner).__name__),
|
||||
# PendingDeprecationWarning, stacklevel=2)
|
||||
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)
|
||||
|
||||
|
||||
|
@ -126,11 +127,10 @@ class Node(object):
|
|||
if isinstance(maybe_compatprop, _CompatProperty):
|
||||
return getattr(__import__("pytest"), name)
|
||||
else:
|
||||
from _pytest.deprecated import CUSTOM_CLASS
|
||||
|
||||
cls = getattr(self, name)
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn("use of node.%s is deprecated, "
|
||||
# "use pytest_pycollect_makeitem(...) to create custom "
|
||||
# "collection nodes" % name, category=DeprecationWarning)
|
||||
self.warn(CUSTOM_CLASS.format(name=name, type_name=type(self).__name__))
|
||||
return cls
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -347,6 +347,9 @@ class Node(object):
|
|||
pass
|
||||
|
||||
def _repr_failure_py(self, excinfo, style=None):
|
||||
if excinfo.errisinstance(fail.Exception):
|
||||
if not excinfo.value.pytrace:
|
||||
return six.text_type(excinfo.value)
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
|
|
|
@ -49,18 +49,24 @@ class Failed(OutcomeException):
|
|||
class Exit(KeyboardInterrupt):
|
||||
""" raised for immediate program exits (no tracebacks/summaries)"""
|
||||
|
||||
def __init__(self, msg="unknown reason"):
|
||||
def __init__(self, msg="unknown reason", returncode=None):
|
||||
self.msg = msg
|
||||
self.returncode = returncode
|
||||
KeyboardInterrupt.__init__(self, msg)
|
||||
|
||||
|
||||
# exposed helper methods
|
||||
|
||||
|
||||
def exit(msg):
|
||||
""" exit testing process as if KeyboardInterrupt was triggered. """
|
||||
def exit(msg, returncode=None):
|
||||
"""
|
||||
Exit testing process as if KeyboardInterrupt was triggered.
|
||||
|
||||
:param str msg: message to display upon exit.
|
||||
:param int returncode: return code to be used when exiting pytest.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise Exit(msg)
|
||||
raise Exit(msg, returncode)
|
||||
|
||||
|
||||
exit.Exception = Exit
|
||||
|
|
|
@ -0,0 +1,283 @@
|
|||
|
||||
import os
|
||||
import errno
|
||||
import atexit
|
||||
import operator
|
||||
import six
|
||||
import sys
|
||||
from functools import reduce
|
||||
import uuid
|
||||
from six.moves import map
|
||||
import itertools
|
||||
import shutil
|
||||
from os.path import expanduser, expandvars, isabs, sep
|
||||
from posixpath import sep as posix_sep
|
||||
import fnmatch
|
||||
import stat
|
||||
|
||||
from .compat import PY36
|
||||
|
||||
|
||||
if PY36:
|
||||
from pathlib import Path, PurePath
|
||||
else:
|
||||
from pathlib2 import Path, PurePath
|
||||
|
||||
__all__ = ["Path", "PurePath"]
|
||||
|
||||
|
||||
LOCK_TIMEOUT = 60 * 60 * 3
|
||||
|
||||
get_lock_path = operator.methodcaller("joinpath", ".lock")
|
||||
|
||||
|
||||
def ensure_reset_dir(path):
|
||||
"""
|
||||
ensures the given path is a empty directory
|
||||
"""
|
||||
if path.exists():
|
||||
rmtree(path, force=True)
|
||||
path.mkdir()
|
||||
|
||||
|
||||
def _shutil_rmtree_remove_writable(func, fspath, _):
|
||||
"Clear the readonly bit and reattempt the removal"
|
||||
os.chmod(fspath, stat.S_IWRITE)
|
||||
func(fspath)
|
||||
|
||||
|
||||
def rmtree(path, force=False):
|
||||
if force:
|
||||
# ignore_errors leaves dead folders around
|
||||
# python needs a rm -rf as a followup
|
||||
# the trick with _shutil_rmtree_remove_writable is unreliable
|
||||
shutil.rmtree(str(path), ignore_errors=True)
|
||||
else:
|
||||
shutil.rmtree(str(path))
|
||||
|
||||
|
||||
def find_prefixed(root, prefix):
|
||||
"""finds all elements in root that begin with the prefix, case insensitive"""
|
||||
l_prefix = prefix.lower()
|
||||
for x in root.iterdir():
|
||||
if x.name.lower().startswith(l_prefix):
|
||||
yield x
|
||||
|
||||
|
||||
def extract_suffixes(iter, prefix):
|
||||
"""
|
||||
:param iter: iterator over path names
|
||||
:param prefix: expected prefix of the path names
|
||||
:returns: the parts of the paths following the prefix
|
||||
"""
|
||||
p_len = len(prefix)
|
||||
for p in iter:
|
||||
yield p.name[p_len:]
|
||||
|
||||
|
||||
def find_suffixes(root, prefix):
|
||||
"""combines find_prefixes and extract_suffixes
|
||||
"""
|
||||
return extract_suffixes(find_prefixed(root, prefix), prefix)
|
||||
|
||||
|
||||
def parse_num(maybe_num):
|
||||
"""parses number path suffixes, returns -1 on error"""
|
||||
try:
|
||||
return int(maybe_num)
|
||||
except ValueError:
|
||||
return -1
|
||||
|
||||
|
||||
if six.PY2:
|
||||
|
||||
def _max(iterable, default):
|
||||
"""needed due to python2.7 lacking the default argument for max"""
|
||||
return reduce(max, iterable, default)
|
||||
|
||||
|
||||
else:
|
||||
_max = max
|
||||
|
||||
|
||||
def make_numbered_dir(root, prefix):
|
||||
"""create a directory with a increased number as suffix for the given prefix"""
|
||||
for i in range(10):
|
||||
# try up to 10 times to create the folder
|
||||
max_existing = _max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
||||
new_number = max_existing + 1
|
||||
new_path = root.joinpath("{}{}".format(prefix, new_number))
|
||||
try:
|
||||
new_path.mkdir()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
return new_path
|
||||
else:
|
||||
raise EnvironmentError(
|
||||
"could not create numbered dir with prefix "
|
||||
"{prefix} in {root} after 10 tries".format(prefix=prefix, root=root)
|
||||
)
|
||||
|
||||
|
||||
def create_cleanup_lock(p):
|
||||
"""crates a lock to prevent premature folder cleanup"""
|
||||
lock_path = get_lock_path(p)
|
||||
try:
|
||||
fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EEXIST:
|
||||
six.raise_from(
|
||||
EnvironmentError("cannot create lockfile in {path}".format(path=p)), e
|
||||
)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
pid = os.getpid()
|
||||
spid = str(pid)
|
||||
if not isinstance(spid, six.binary_type):
|
||||
spid = spid.encode("ascii")
|
||||
os.write(fd, spid)
|
||||
os.close(fd)
|
||||
if not lock_path.is_file():
|
||||
raise EnvironmentError("lock path got renamed after sucessfull creation")
|
||||
return lock_path
|
||||
|
||||
|
||||
def register_cleanup_lock_removal(lock_path, register=atexit.register):
|
||||
"""registers a cleanup function for removing a lock, by default on atexit"""
|
||||
pid = os.getpid()
|
||||
|
||||
def cleanup_on_exit(lock_path=lock_path, original_pid=pid):
|
||||
current_pid = os.getpid()
|
||||
if current_pid != original_pid:
|
||||
# fork
|
||||
return
|
||||
try:
|
||||
lock_path.unlink()
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
return register(cleanup_on_exit)
|
||||
|
||||
|
||||
def delete_a_numbered_dir(path):
|
||||
"""removes a numbered directory"""
|
||||
create_cleanup_lock(path)
|
||||
parent = path.parent
|
||||
|
||||
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
|
||||
path.rename(garbage)
|
||||
rmtree(garbage, force=True)
|
||||
|
||||
|
||||
def ensure_deletable(path, consider_lock_dead_if_created_before):
|
||||
"""checks if a lock exists and breaks it if its considered dead"""
|
||||
if path.is_symlink():
|
||||
return False
|
||||
lock = get_lock_path(path)
|
||||
if not lock.exists():
|
||||
return True
|
||||
try:
|
||||
lock_time = lock.stat().st_mtime
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
if lock_time < consider_lock_dead_if_created_before:
|
||||
lock.unlink()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def try_cleanup(path, consider_lock_dead_if_created_before):
|
||||
"""tries to cleanup a folder if we can ensure its deletable"""
|
||||
if ensure_deletable(path, consider_lock_dead_if_created_before):
|
||||
delete_a_numbered_dir(path)
|
||||
|
||||
|
||||
def cleanup_candidates(root, prefix, keep):
|
||||
"""lists candidates for numbered directories to be removed - follows py.path"""
|
||||
max_existing = _max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
||||
max_delete = max_existing - keep
|
||||
paths = find_prefixed(root, prefix)
|
||||
paths, paths2 = itertools.tee(paths)
|
||||
numbers = map(parse_num, extract_suffixes(paths2, prefix))
|
||||
for path, number in zip(paths, numbers):
|
||||
if number <= max_delete:
|
||||
yield path
|
||||
|
||||
|
||||
def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before):
|
||||
"""cleanup for lock driven numbered directories"""
|
||||
for path in cleanup_candidates(root, prefix, keep):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
for path in root.glob("garbage-*"):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
|
||||
|
||||
def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout):
|
||||
"""creates a numbered dir with a cleanup lock and removes old ones"""
|
||||
e = None
|
||||
for i in range(10):
|
||||
try:
|
||||
p = make_numbered_dir(root, prefix)
|
||||
lock_path = create_cleanup_lock(p)
|
||||
register_cleanup_lock_removal(lock_path)
|
||||
except Exception as e:
|
||||
pass
|
||||
else:
|
||||
consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout
|
||||
cleanup_numbered_dir(
|
||||
root=root,
|
||||
prefix=prefix,
|
||||
keep=keep,
|
||||
consider_lock_dead_if_created_before=consider_lock_dead_if_created_before,
|
||||
)
|
||||
return p
|
||||
assert e is not None
|
||||
raise e
|
||||
|
||||
|
||||
def resolve_from_str(input, root):
|
||||
assert not isinstance(input, Path), "would break on py2"
|
||||
root = Path(root)
|
||||
input = expanduser(input)
|
||||
input = expandvars(input)
|
||||
if isabs(input):
|
||||
return Path(input)
|
||||
else:
|
||||
return root.joinpath(input)
|
||||
|
||||
|
||||
def fnmatch_ex(pattern, path):
|
||||
"""FNMatcher port from py.path.common which works with PurePath() instances.
|
||||
|
||||
The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions
|
||||
for each part of the path, while this algorithm uses the whole path instead.
|
||||
|
||||
For example:
|
||||
"tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with
|
||||
PurePath.match().
|
||||
|
||||
This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according
|
||||
this logic.
|
||||
|
||||
References:
|
||||
* https://bugs.python.org/issue29249
|
||||
* https://bugs.python.org/issue34731
|
||||
"""
|
||||
path = PurePath(path)
|
||||
iswin32 = sys.platform.startswith("win")
|
||||
|
||||
if iswin32 and sep not in pattern and posix_sep in pattern:
|
||||
# Running on Windows, the pattern has no Windows path separators,
|
||||
# and the pattern has one or more Posix path separators. Replace
|
||||
# the Posix path separators with the Windows path separator.
|
||||
pattern = pattern.replace(posix_sep, sep)
|
||||
|
||||
if sep not in pattern:
|
||||
name = path.name
|
||||
else:
|
||||
name = six.text_type(path)
|
||||
return fnmatch.fnmatch(name, pattern)
|
|
@ -1,52 +0,0 @@
|
|||
from os.path import expanduser, expandvars, isabs, sep
|
||||
from posixpath import sep as posix_sep
|
||||
import fnmatch
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
from .compat import Path, PurePath
|
||||
|
||||
|
||||
def resolve_from_str(input, root):
|
||||
assert not isinstance(input, Path), "would break on py2"
|
||||
root = Path(root)
|
||||
input = expanduser(input)
|
||||
input = expandvars(input)
|
||||
if isabs(input):
|
||||
return Path(input)
|
||||
else:
|
||||
return root.joinpath(input)
|
||||
|
||||
|
||||
def fnmatch_ex(pattern, path):
|
||||
"""FNMatcher port from py.path.common which works with PurePath() instances.
|
||||
|
||||
The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions
|
||||
for each part of the path, while this algorithm uses the whole path instead.
|
||||
|
||||
For example:
|
||||
"tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with
|
||||
PurePath.match().
|
||||
|
||||
This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according
|
||||
this logic.
|
||||
|
||||
References:
|
||||
* https://bugs.python.org/issue29249
|
||||
* https://bugs.python.org/issue34731
|
||||
"""
|
||||
path = PurePath(path)
|
||||
iswin32 = sys.platform.startswith("win")
|
||||
|
||||
if iswin32 and sep not in pattern and posix_sep in pattern:
|
||||
# Running on Windows, the pattern has no Windows path separators,
|
||||
# and the pattern has one or more Posix path separators. Replace
|
||||
# the Posix path separators with the Windows path separator.
|
||||
pattern = pattern.replace(posix_sep, sep)
|
||||
|
||||
if sep not in pattern:
|
||||
name = path.name
|
||||
else:
|
||||
name = six.text_type(path)
|
||||
return fnmatch.fnmatch(name, pattern)
|
|
@ -17,13 +17,14 @@ from weakref import WeakKeyDictionary
|
|||
|
||||
from _pytest.capture import MultiCapture, SysCapture
|
||||
from _pytest._code import Source
|
||||
import py
|
||||
import pytest
|
||||
from _pytest.main import Session, EXIT_INTERRUPTED, EXIT_OK
|
||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||
from _pytest.compat import Path
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.compat import safe_str
|
||||
|
||||
import py
|
||||
import pytest
|
||||
|
||||
IGNORE_PAM = [ # filenames added when obtaining details about the current user
|
||||
u"/var/lib/sss/mc/passwd"
|
||||
]
|
||||
|
@ -61,6 +62,11 @@ def pytest_configure(config):
|
|||
config.pluginmanager.register(checker)
|
||||
|
||||
|
||||
def raise_on_kwargs(kwargs):
|
||||
if kwargs:
|
||||
raise TypeError("Unexpected arguments: {}".format(", ".join(sorted(kwargs))))
|
||||
|
||||
|
||||
class LsofFdLeakChecker(object):
|
||||
def get_open_files(self):
|
||||
out = self._exec_lsof()
|
||||
|
@ -482,11 +488,16 @@ class Testdir(object):
|
|||
|
||||
"""
|
||||
|
||||
class TimeoutExpired(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, request, tmpdir_factory):
|
||||
self.request = request
|
||||
self._mod_collections = WeakKeyDictionary()
|
||||
name = request.function.__name__
|
||||
self.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
|
||||
self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True)
|
||||
os.environ["PYTEST_DEBUG_TEMPROOT"] = str(self.test_tmproot)
|
||||
self.plugins = []
|
||||
self._cwd_snapshot = CwdSnapshot()
|
||||
self._sys_path_snapshot = SysPathsSnapshot()
|
||||
|
@ -513,6 +524,7 @@ class Testdir(object):
|
|||
self._sys_modules_snapshot.restore()
|
||||
self._sys_path_snapshot.restore()
|
||||
self._cwd_snapshot.restore()
|
||||
os.environ.pop("PYTEST_DEBUG_TEMPROOT", None)
|
||||
|
||||
def __take_sys_modules_snapshot(self):
|
||||
# some zope modules used by twisted-related tests keep internal state
|
||||
|
@ -1039,14 +1051,23 @@ class Testdir(object):
|
|||
|
||||
return popen
|
||||
|
||||
def run(self, *cmdargs):
|
||||
def run(self, *cmdargs, **kwargs):
|
||||
"""Run a command with arguments.
|
||||
|
||||
Run a process using subprocess.Popen saving the stdout and stderr.
|
||||
|
||||
:param args: the sequence of arguments to pass to `subprocess.Popen()`
|
||||
:param timeout: the period in seconds after which to timeout and raise
|
||||
:py:class:`Testdir.TimeoutExpired`
|
||||
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
|
||||
timeout = kwargs.pop("timeout", None)
|
||||
raise_on_kwargs(kwargs)
|
||||
|
||||
cmdargs = [
|
||||
str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs
|
||||
]
|
||||
|
@ -1061,7 +1082,40 @@ class Testdir(object):
|
|||
popen = self.popen(
|
||||
cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32")
|
||||
)
|
||||
|
||||
def handle_timeout():
|
||||
__tracebackhide__ = True
|
||||
|
||||
timeout_message = (
|
||||
"{seconds} second timeout expired running:"
|
||||
" {command}".format(seconds=timeout, command=cmdargs)
|
||||
)
|
||||
|
||||
popen.kill()
|
||||
popen.wait()
|
||||
raise self.TimeoutExpired(timeout_message)
|
||||
|
||||
if timeout is None:
|
||||
ret = popen.wait()
|
||||
elif six.PY3:
|
||||
try:
|
||||
ret = popen.wait(timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
handle_timeout()
|
||||
else:
|
||||
end = time.time() + timeout
|
||||
|
||||
resolution = min(0.1, timeout / 10)
|
||||
|
||||
while True:
|
||||
ret = popen.poll()
|
||||
if ret is not None:
|
||||
break
|
||||
|
||||
if time.time() > end:
|
||||
handle_timeout()
|
||||
|
||||
time.sleep(resolution)
|
||||
finally:
|
||||
f1.close()
|
||||
f2.close()
|
||||
|
@ -1108,9 +1162,15 @@ class Testdir(object):
|
|||
with "runpytest-" so they do not conflict with the normal numbered
|
||||
pytest location for temporary files and directories.
|
||||
|
||||
:param args: the sequence of arguments to pass to the pytest subprocess
|
||||
:param timeout: the period in seconds after which to timeout and raise
|
||||
:py:class:`Testdir.TimeoutExpired`
|
||||
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
|
||||
p = py.path.local.make_numbered_dir(
|
||||
prefix="runpytest-", keep=None, rootdir=self.tmpdir
|
||||
)
|
||||
|
@ -1119,7 +1179,7 @@ class Testdir(object):
|
|||
if plugins:
|
||||
args = ("-p", plugins[0]) + args
|
||||
args = self._getpytestargs() + args
|
||||
return self.run(*args)
|
||||
return self.run(*args, timeout=kwargs.get("timeout"))
|
||||
|
||||
def spawn_pytest(self, string, expect_timeout=10.0):
|
||||
"""Run pytest using pexpect.
|
||||
|
@ -1267,6 +1327,7 @@ class LineMatcher(object):
|
|||
matches and non-matches are also printed on stdout.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
self._match_lines(lines2, fnmatch, "fnmatch")
|
||||
|
||||
def re_match_lines(self, lines2):
|
||||
|
@ -1278,6 +1339,7 @@ class LineMatcher(object):
|
|||
The matches and non-matches are also printed on stdout.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match")
|
||||
|
||||
def _match_lines(self, lines2, match_func, match_nickname):
|
||||
|
|
|
@ -13,11 +13,10 @@ from textwrap import dedent
|
|||
import py
|
||||
import six
|
||||
from _pytest.main import FSHookProxy
|
||||
from _pytest.mark import MarkerError
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
import _pytest
|
||||
import pluggy
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest import fixtures
|
||||
from _pytest import nodes
|
||||
from _pytest import deprecated
|
||||
|
@ -47,37 +46,6 @@ from _pytest.mark.structures import (
|
|||
)
|
||||
from _pytest.warning_types import RemovedInPytest4Warning, PytestWarning
|
||||
|
||||
# relative paths that we use to filter traceback entries from appearing to the user;
|
||||
# see filter_traceback
|
||||
# note: if we need to add more paths than what we have now we should probably use a list
|
||||
# for better maintenance
|
||||
_pluggy_dir = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||
# pluggy is either a package or a single module depending on the version
|
||||
if _pluggy_dir.basename == "__init__.py":
|
||||
_pluggy_dir = _pluggy_dir.dirpath()
|
||||
_pytest_dir = py.path.local(_pytest.__file__).dirpath()
|
||||
_py_dir = py.path.local(py.__file__).dirpath()
|
||||
|
||||
|
||||
def filter_traceback(entry):
|
||||
"""Return True if a TracebackEntry instance should be removed from tracebacks:
|
||||
* dynamically generated code (no code to show up for it);
|
||||
* internal traceback from pytest or its internal libraries, py and pluggy.
|
||||
"""
|
||||
# entry.path might sometimes return a str object when the entry
|
||||
# points to dynamically generated code
|
||||
# see https://bitbucket.org/pytest-dev/py/issues/71
|
||||
raw_filename = entry.frame.code.raw.co_filename
|
||||
is_generated = "<" in raw_filename and ">" in raw_filename
|
||||
if is_generated:
|
||||
return False
|
||||
# entry.path might point to a non-existing file, in which case it will
|
||||
# also return a str object. see #1133
|
||||
p = py.path.local(entry.path)
|
||||
return (
|
||||
not p.relto(_pluggy_dir) and not p.relto(_pytest_dir) and not p.relto(_py_dir)
|
||||
)
|
||||
|
||||
|
||||
def pyobj_property(name):
|
||||
def get(self):
|
||||
|
@ -159,8 +127,8 @@ def pytest_generate_tests(metafunc):
|
|||
alt_spellings = ["parameterize", "parametrise", "parameterise"]
|
||||
for attr in alt_spellings:
|
||||
if hasattr(metafunc.function, attr):
|
||||
msg = "{0} has '{1}', spelling should be 'parametrize'"
|
||||
raise MarkerError(msg.format(metafunc.function.__name__, attr))
|
||||
msg = "{0} has '{1}' mark, spelling should be 'parametrize'"
|
||||
fail(msg.format(metafunc.function.__name__, attr), pytrace=False)
|
||||
for marker in metafunc.definition.iter_markers(name="parametrize"):
|
||||
metafunc.parametrize(*marker.args, **marker.kwargs)
|
||||
|
||||
|
@ -760,12 +728,6 @@ class FunctionMixin(PyobjMixin):
|
|||
for entry in excinfo.traceback[1:-1]:
|
||||
entry.set_repr_style("short")
|
||||
|
||||
def _repr_failure_py(self, excinfo, style="long"):
|
||||
if excinfo.errisinstance(fail.Exception):
|
||||
if not excinfo.value.pytrace:
|
||||
return six.text_type(excinfo.value)
|
||||
return super(FunctionMixin, self)._repr_failure_py(excinfo, style=style)
|
||||
|
||||
def repr_failure(self, excinfo, outerr=None):
|
||||
assert outerr is None, "XXX outerr usage is deprecated"
|
||||
style = self.config.option.tbstyle
|
||||
|
@ -799,7 +761,10 @@ class Generator(FunctionMixin, PyCollector):
|
|||
"%r generated tests with non-unique name %r" % (self, name)
|
||||
)
|
||||
seen[name] = True
|
||||
values.append(self.Function(name, self, args=args, callobj=call))
|
||||
with warnings.catch_warnings():
|
||||
# ignore our own deprecation warning
|
||||
function_class = self.Function
|
||||
values.append(function_class(name, self, args=args, callobj=call))
|
||||
self.warn(deprecated.YIELD_TESTS)
|
||||
return values
|
||||
|
||||
|
@ -984,7 +949,9 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
|||
|
||||
ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition)
|
||||
|
||||
scopenum = scope2index(scope, descr="call to {}".format(self.parametrize))
|
||||
scopenum = scope2index(
|
||||
scope, descr="parametrize() call in {}".format(self.function.__name__)
|
||||
)
|
||||
|
||||
# create the new calls: if we are parametrize() multiple times (by applying the decorator
|
||||
# more than once) then we accumulate those calls generating the cartesian product
|
||||
|
@ -1023,15 +990,16 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
|||
idfn = ids
|
||||
ids = None
|
||||
if ids:
|
||||
func_name = self.function.__name__
|
||||
if len(ids) != len(parameters):
|
||||
raise ValueError(
|
||||
"%d tests specified with %d ids" % (len(parameters), len(ids))
|
||||
)
|
||||
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
|
||||
fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False)
|
||||
for id_value in ids:
|
||||
if id_value is not None and not isinstance(id_value, six.string_types):
|
||||
msg = "ids must be list of strings, found: %s (type: %s)"
|
||||
raise ValueError(
|
||||
msg % (saferepr(id_value), type(id_value).__name__)
|
||||
msg = "In {}: ids must be list of strings, found: {} (type: {!r})"
|
||||
fail(
|
||||
msg.format(func_name, saferepr(id_value), type(id_value)),
|
||||
pytrace=False,
|
||||
)
|
||||
ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item)
|
||||
return ids
|
||||
|
@ -1056,9 +1024,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
|||
valtypes = dict.fromkeys(argnames, "funcargs")
|
||||
for arg in indirect:
|
||||
if arg not in argnames:
|
||||
raise ValueError(
|
||||
"indirect given to %r: fixture %r doesn't exist"
|
||||
% (self.function, arg)
|
||||
fail(
|
||||
"In {}: indirect fixture '{}' doesn't exist".format(
|
||||
self.function.__name__, arg
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
valtypes[arg] = "params"
|
||||
return valtypes
|
||||
|
@ -1072,19 +1042,25 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
|||
:raise ValueError: if validation fails.
|
||||
"""
|
||||
default_arg_names = set(get_default_arg_names(self.function))
|
||||
func_name = self.function.__name__
|
||||
for arg in argnames:
|
||||
if arg not in self.fixturenames:
|
||||
if arg in default_arg_names:
|
||||
raise ValueError(
|
||||
"%r already takes an argument %r with a default value"
|
||||
% (self.function, arg)
|
||||
fail(
|
||||
"In {}: function already takes an argument '{}' with a default value".format(
|
||||
func_name, arg
|
||||
),
|
||||
pytrace=False,
|
||||
)
|
||||
else:
|
||||
if isinstance(indirect, (tuple, list)):
|
||||
name = "fixture" if arg in indirect else "argument"
|
||||
else:
|
||||
name = "fixture" if indirect else "argument"
|
||||
raise ValueError("%r uses no %s %r" % (self.function, name, arg))
|
||||
fail(
|
||||
"In {}: function uses no {} '{}'".format(func_name, name, arg),
|
||||
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.
|
||||
|
|
|
@ -43,45 +43,10 @@ def deprecated_call(func=None, *args, **kwargs):
|
|||
in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
|
||||
types above.
|
||||
"""
|
||||
if not func:
|
||||
return _DeprecatedCallContext()
|
||||
else:
|
||||
__tracebackhide__ = True
|
||||
with _DeprecatedCallContext():
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
class _DeprecatedCallContext(object):
|
||||
"""Implements the logic to capture deprecation warnings as a context manager."""
|
||||
|
||||
def __enter__(self):
|
||||
self._captured_categories = []
|
||||
self._old_warn = warnings.warn
|
||||
self._old_warn_explicit = warnings.warn_explicit
|
||||
warnings.warn_explicit = self._warn_explicit
|
||||
warnings.warn = self._warn
|
||||
|
||||
def _warn_explicit(self, message, category, *args, **kwargs):
|
||||
self._captured_categories.append(category)
|
||||
|
||||
def _warn(self, message, category=None, *args, **kwargs):
|
||||
if isinstance(message, Warning):
|
||||
self._captured_categories.append(message.__class__)
|
||||
else:
|
||||
self._captured_categories.append(category)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
warnings.warn_explicit = self._old_warn_explicit
|
||||
warnings.warn = self._old_warn
|
||||
|
||||
if exc_type is None:
|
||||
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
|
||||
if not any(
|
||||
issubclass(c, deprecation_categories) for c in self._captured_categories
|
||||
):
|
||||
__tracebackhide__ = True
|
||||
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
|
||||
raise AssertionError(msg)
|
||||
if func is not None:
|
||||
args = (func,) + args
|
||||
return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs)
|
||||
|
||||
|
||||
def warns(expected_warning, *args, **kwargs):
|
||||
|
@ -116,6 +81,7 @@ def warns(expected_warning, *args, **kwargs):
|
|||
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
match_expr = None
|
||||
if not args:
|
||||
if "match" in kwargs:
|
||||
|
@ -183,12 +149,25 @@ class WarningsRecorder(warnings.catch_warnings):
|
|||
raise RuntimeError("Cannot enter %r twice" % self)
|
||||
self._list = super(WarningsRecorder, self).__enter__()
|
||||
warnings.simplefilter("always")
|
||||
# python3 keeps track of a "filter version", when the filters are
|
||||
# updated previously seen warnings can be re-warned. python2 has no
|
||||
# concept of this so we must reset the warnings registry manually.
|
||||
# trivial patching of `warnings.warn` seems to be enough somehow?
|
||||
if six.PY2:
|
||||
|
||||
def warn(*args, **kwargs):
|
||||
return self._saved_warn(*args, **kwargs)
|
||||
|
||||
warnings.warn, self._saved_warn = warn, warnings.warn
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
if not self._entered:
|
||||
__tracebackhide__ = True
|
||||
raise RuntimeError("Cannot exit %r without entering first" % self)
|
||||
# see above where `self._saved_warn` is assigned
|
||||
if six.PY2:
|
||||
warnings.warn = self._saved_warn
|
||||
super(WarningsRecorder, self).__exit__(*exc_info)
|
||||
|
||||
|
||||
|
|
|
@ -31,10 +31,9 @@ def pytest_configure(config):
|
|||
config.pluginmanager.register(config._resultlog)
|
||||
|
||||
from _pytest.deprecated import RESULT_LOG
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
||||
_issue_config_warning(RemovedInPytest4Warning(RESULT_LOG), config)
|
||||
_issue_config_warning(RESULT_LOG, config)
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
|
|
|
@ -676,7 +676,9 @@ class TerminalReporter(object):
|
|||
|
||||
if fspath:
|
||||
res = mkrel(nodeid).replace("::()", "") # parens-normalization
|
||||
if nodeid.split("::")[0] != fspath.replace("\\", nodes.SEP):
|
||||
if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
|
||||
"\\", nodes.SEP
|
||||
):
|
||||
res += " <- " + self.startdir.bestrelpath(fspath)
|
||||
else:
|
||||
res = "[location]"
|
||||
|
|
|
@ -1,22 +1,86 @@
|
|||
""" support for providing temporary directories to test functions. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import py
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
import attr
|
||||
import tempfile
|
||||
import warnings
|
||||
|
||||
from .pathlib import (
|
||||
Path,
|
||||
make_numbered_dir,
|
||||
make_numbered_dir_with_cleanup,
|
||||
ensure_reset_dir,
|
||||
LOCK_TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
class TempdirFactory(object):
|
||||
@attr.s
|
||||
class TempPathFactory(object):
|
||||
"""Factory for temporary directories under the common base temp directory.
|
||||
|
||||
The base directory can be configured using the ``--basetemp`` option.
|
||||
The base directory can be configured using the ``--basetemp`` option."""
|
||||
|
||||
_given_basetemp = attr.ib()
|
||||
_trace = attr.ib()
|
||||
_basetemp = attr.ib(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config):
|
||||
"""
|
||||
:param config: a pytest configuration
|
||||
"""
|
||||
return cls(
|
||||
given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
|
||||
)
|
||||
|
||||
def mktemp(self, basename, numbered=True):
|
||||
"""makes a temporary directory managed by the factory"""
|
||||
if not numbered:
|
||||
p = self.getbasetemp().joinpath(basename)
|
||||
p.mkdir()
|
||||
else:
|
||||
p = make_numbered_dir(root=self.getbasetemp(), prefix=basename)
|
||||
self._trace("mktemp", p)
|
||||
return p
|
||||
|
||||
def getbasetemp(self):
|
||||
""" return base temporary directory. """
|
||||
if self._basetemp is None:
|
||||
if self._given_basetemp is not None:
|
||||
basetemp = Path(self._given_basetemp)
|
||||
ensure_reset_dir(basetemp)
|
||||
else:
|
||||
from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
|
||||
temproot = Path(from_env or tempfile.gettempdir())
|
||||
user = get_user() or "unknown"
|
||||
# use a sub-directory in the temproot to speed-up
|
||||
# make_numbered_dir() call
|
||||
rootdir = temproot.joinpath("pytest-of-{}".format(user))
|
||||
rootdir.mkdir(exist_ok=True)
|
||||
basetemp = make_numbered_dir_with_cleanup(
|
||||
prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT
|
||||
)
|
||||
assert basetemp is not None
|
||||
self._basetemp = t = basetemp
|
||||
self._trace("new basetemp", t)
|
||||
return t
|
||||
else:
|
||||
return self._basetemp
|
||||
|
||||
|
||||
@attr.s
|
||||
class TempdirFactory(object):
|
||||
"""
|
||||
backward comptibility wrapper that implements
|
||||
:class:``py.path.local`` for :class:``TempPathFactory``
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.trace = config.trace.get("tmpdir")
|
||||
_tmppath_factory = attr.ib()
|
||||
|
||||
def ensuretemp(self, string, dir=1):
|
||||
""" (deprecated) return temporary directory path with
|
||||
|
@ -26,6 +90,9 @@ class TempdirFactory(object):
|
|||
and is guaranteed to be empty.
|
||||
"""
|
||||
# py.log._apiwarn(">1.1", "use tmpdir function argument")
|
||||
from .deprecated import PYTEST_ENSURETEMP
|
||||
|
||||
warnings.warn(PYTEST_ENSURETEMP, stacklevel=2)
|
||||
return self.getbasetemp().ensure(string, dir=dir)
|
||||
|
||||
def mktemp(self, basename, numbered=True):
|
||||
|
@ -33,46 +100,11 @@ class TempdirFactory(object):
|
|||
If ``numbered``, ensure the directory is unique by adding a number
|
||||
prefix greater than any existing one.
|
||||
"""
|
||||
basetemp = self.getbasetemp()
|
||||
if not numbered:
|
||||
p = basetemp.mkdir(basename)
|
||||
else:
|
||||
p = py.path.local.make_numbered_dir(
|
||||
prefix=basename, keep=0, rootdir=basetemp, lock_timeout=None
|
||||
)
|
||||
self.trace("mktemp", p)
|
||||
return p
|
||||
return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())
|
||||
|
||||
def getbasetemp(self):
|
||||
""" return base temporary directory. """
|
||||
try:
|
||||
return self._basetemp
|
||||
except AttributeError:
|
||||
basetemp = self.config.option.basetemp
|
||||
if basetemp:
|
||||
basetemp = py.path.local(basetemp)
|
||||
if basetemp.check():
|
||||
basetemp.remove()
|
||||
basetemp.mkdir()
|
||||
else:
|
||||
temproot = py.path.local.get_temproot()
|
||||
user = get_user()
|
||||
if user:
|
||||
# use a sub-directory in the temproot to speed-up
|
||||
# make_numbered_dir() call
|
||||
rootdir = temproot.join("pytest-of-%s" % user)
|
||||
else:
|
||||
rootdir = temproot
|
||||
rootdir.ensure(dir=1)
|
||||
basetemp = py.path.local.make_numbered_dir(
|
||||
prefix="pytest-", rootdir=rootdir
|
||||
)
|
||||
self._basetemp = t = basetemp.realpath()
|
||||
self.trace("new basetemp", t)
|
||||
return t
|
||||
|
||||
def finish(self):
|
||||
self.trace("finish")
|
||||
"""backward compat wrapper for ``_tmppath_factory.getbasetemp``"""
|
||||
return py.path.local(self._tmppath_factory.getbasetemp().resolve())
|
||||
|
||||
|
||||
def get_user():
|
||||
|
@ -87,10 +119,6 @@ def get_user():
|
|||
return None
|
||||
|
||||
|
||||
# backward compatibility
|
||||
TempdirHandler = TempdirFactory
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Create a TempdirFactory and attach it to the config object.
|
||||
|
||||
|
@ -99,19 +127,36 @@ def pytest_configure(config):
|
|||
to the tmpdir_factory session fixture.
|
||||
"""
|
||||
mp = MonkeyPatch()
|
||||
t = TempdirFactory(config)
|
||||
config._cleanup.extend([mp.undo, t.finish])
|
||||
tmppath_handler = TempPathFactory.from_config(config)
|
||||
t = TempdirFactory(tmppath_handler)
|
||||
config._cleanup.append(mp.undo)
|
||||
mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False)
|
||||
mp.setattr(config, "_tmpdirhandler", t, raising=False)
|
||||
mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tmpdir_factory(request):
|
||||
"""Return a TempdirFactory instance for the test session.
|
||||
"""Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
|
||||
"""
|
||||
return request.config._tmpdirhandler
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tmp_path_factory(request):
|
||||
"""Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
|
||||
"""
|
||||
return request.config._tmp_path_factory
|
||||
|
||||
|
||||
def _mk_tmp(request, factory):
|
||||
name = request.node.name
|
||||
name = re.sub(r"[\W]", "_", name)
|
||||
MAXVAL = 30
|
||||
name = name[:MAXVAL]
|
||||
return factory.mktemp(name, numbered=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmpdir(request, tmpdir_factory):
|
||||
"""Return a temporary directory path object
|
||||
|
@ -122,10 +167,20 @@ def tmpdir(request, tmpdir_factory):
|
|||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
"""
|
||||
name = request.node.name
|
||||
name = re.sub(r"[\W]", "_", name)
|
||||
MAXVAL = 30
|
||||
if len(name) > MAXVAL:
|
||||
name = name[:MAXVAL]
|
||||
x = tmpdir_factory.mktemp(name, numbered=True)
|
||||
return x
|
||||
return _mk_tmp(request, tmpdir_factory)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_path(request, tmp_path_factory):
|
||||
"""Return a temporary directory path object
|
||||
which is unique to each test function invocation,
|
||||
created as a sub directory of the base temporary
|
||||
directory. The returned object is a :class:`pathlib.Path`
|
||||
object.
|
||||
|
||||
.. note::
|
||||
|
||||
in python < 3.6 this is a pathlib2.Path
|
||||
"""
|
||||
|
||||
return _mk_tmp(request, tmp_path_factory)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import attr
|
||||
|
||||
|
||||
class PytestWarning(UserWarning):
|
||||
"""
|
||||
Bases: :class:`UserWarning`.
|
||||
|
@ -39,4 +42,19 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
|
|||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class UnformattedWarning(object):
|
||||
"""Used to hold warnings that need to format their message at runtime, as opposed to a direct message.
|
||||
|
||||
Using this class avoids to keep all the warning types and messages in this module, avoiding misuse.
|
||||
"""
|
||||
|
||||
category = attr.ib()
|
||||
template = attr.ib()
|
||||
|
||||
def format(self, **kwargs):
|
||||
"""Returns an instance of the warning category, formatted with given kwargs"""
|
||||
return self.category(self.template.format(**kwargs))
|
||||
|
||||
|
||||
PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example")
|
||||
|
|
|
@ -67,27 +67,27 @@ def catch_warnings_for_item(config, ihook, when, item):
|
|||
|
||||
Each warning captured triggers the ``pytest_warning_captured`` hook.
|
||||
"""
|
||||
args = config.getoption("pythonwarnings") or []
|
||||
cmdline_filters = config.getoption("pythonwarnings") or []
|
||||
inifilters = config.getini("filterwarnings")
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
filters_configured = args or inifilters or sys.warnoptions
|
||||
|
||||
for arg in args:
|
||||
warnings._setoption(arg)
|
||||
if not sys.warnoptions:
|
||||
# if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
|
||||
warnings.filterwarnings("always", category=DeprecationWarning)
|
||||
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
||||
|
||||
# filters should have this precedence: mark, cmdline options, ini
|
||||
# filters should be applied in the inverse order of precedence
|
||||
for arg in inifilters:
|
||||
_setoption(warnings, arg)
|
||||
|
||||
for arg in cmdline_filters:
|
||||
warnings._setoption(arg)
|
||||
|
||||
if item is not None:
|
||||
for mark in item.iter_markers(name="filterwarnings"):
|
||||
for arg in mark.args:
|
||||
_setoption(warnings, arg)
|
||||
filters_configured = True
|
||||
|
||||
if not filters_configured:
|
||||
# if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
|
||||
warnings.filterwarnings("always", category=DeprecationWarning)
|
||||
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
||||
|
||||
yield
|
||||
|
||||
|
|
|
@ -140,9 +140,16 @@ class TestGeneralUsage(object):
|
|||
assert result.ret
|
||||
result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)])
|
||||
|
||||
def test_issue486_better_reporting_on_conftest_load_failure(self, testdir):
|
||||
def test_better_reporting_on_conftest_load_failure(self, testdir, request):
|
||||
"""Show a user-friendly traceback on conftest import failures (#486, #3332)"""
|
||||
testdir.makepyfile("")
|
||||
testdir.makeconftest("import qwerty")
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
def foo():
|
||||
import qwerty
|
||||
foo()
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("--help")
|
||||
result.stdout.fnmatch_lines(
|
||||
"""
|
||||
|
@ -151,10 +158,23 @@ class TestGeneralUsage(object):
|
|||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
dirname = request.node.name + "0"
|
||||
exc_name = (
|
||||
"ModuleNotFoundError" if sys.version_info >= (3, 6) else "ImportError"
|
||||
)
|
||||
result.stderr.fnmatch_lines(
|
||||
"""
|
||||
*ERROR*could not load*conftest.py*
|
||||
"""
|
||||
[
|
||||
"ImportError while loading conftest '*{sep}{dirname}{sep}conftest.py'.".format(
|
||||
dirname=dirname, sep=os.sep
|
||||
),
|
||||
"conftest.py:3: in <module>",
|
||||
" foo()",
|
||||
"conftest.py:2: in foo",
|
||||
" import qwerty",
|
||||
"E {}: No module named {q}qwerty{q}".format(
|
||||
exc_name, q="'" if six.PY3 else ""
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def test_early_skip(self, testdir):
|
||||
|
@ -723,9 +743,19 @@ class TestInvocationVariants(object):
|
|||
monkeypatch.syspath_prepend(p)
|
||||
|
||||
# module picked up in symlink-ed directory:
|
||||
# It picks up local/lib/foo/bar (symlink) via sys.path.
|
||||
result = testdir.runpytest("--pyargs", "-v", "foo.bar")
|
||||
testdir.chdir()
|
||||
assert result.ret == 0
|
||||
if hasattr(py.path.local, "mksymlinkto"):
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"lib/foo/bar/test_bar.py::test_bar PASSED*",
|
||||
"lib/foo/bar/test_bar.py::test_other PASSED*",
|
||||
"*2 passed*",
|
||||
]
|
||||
)
|
||||
else:
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*lib/foo/bar/test_bar.py::test_bar PASSED*",
|
||||
|
|
|
@ -1184,20 +1184,28 @@ raise ValueError()
|
|||
assert tw.lines[47] == ":15: AttributeError"
|
||||
|
||||
@pytest.mark.skipif("sys.version_info[0] < 3")
|
||||
def test_exc_repr_with_raise_from_none_chain_suppression(self, importasmod):
|
||||
@pytest.mark.parametrize("mode", ["from_none", "explicit_suppress"])
|
||||
def test_exc_repr_chain_suppression(self, importasmod, mode):
|
||||
"""Check that exc repr does not show chained exceptions in Python 3.
|
||||
- When the exception is raised with "from None"
|
||||
- Explicitly suppressed with "chain=False" to ExceptionInfo.getrepr().
|
||||
"""
|
||||
raise_suffix = " from None" if mode == "from_none" else ""
|
||||
mod = importasmod(
|
||||
"""
|
||||
def f():
|
||||
try:
|
||||
g()
|
||||
except Exception:
|
||||
raise AttributeError() from None
|
||||
raise AttributeError(){raise_suffix}
|
||||
def g():
|
||||
raise ValueError()
|
||||
"""
|
||||
""".format(
|
||||
raise_suffix=raise_suffix
|
||||
)
|
||||
)
|
||||
excinfo = pytest.raises(AttributeError, mod.f)
|
||||
r = excinfo.getrepr(style="long")
|
||||
r = excinfo.getrepr(style="long", chain=mode != "explicit_suppress")
|
||||
tw = TWMock()
|
||||
r.toterminal(tw)
|
||||
for line in tw.lines:
|
||||
|
@ -1207,7 +1215,9 @@ raise ValueError()
|
|||
assert tw.lines[2] == " try:"
|
||||
assert tw.lines[3] == " g()"
|
||||
assert tw.lines[4] == " except Exception:"
|
||||
assert tw.lines[5] == "> raise AttributeError() from None"
|
||||
assert tw.lines[5] == "> raise AttributeError(){}".format(
|
||||
raise_suffix
|
||||
)
|
||||
assert tw.lines[6] == "E AttributeError"
|
||||
assert tw.lines[7] == ""
|
||||
line = tw.get_write_msg(8)
|
||||
|
|
|
@ -30,6 +30,74 @@ def test_yield_tests_deprecation(testdir):
|
|||
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()
|
||||
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()
|
||||
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()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
'*test_custom_class_deprecation.py:1:*"Class" objects in collectors of type "MyModule*',
|
||||
"*1 passed, 1 warnings in*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_funcarg_prefix_deprecation(testdir):
|
||||
testdir.makepyfile(
|
||||
|
|
|
@ -908,3 +908,61 @@ def test_live_logging_suspends_capture(has_capture_manager, request):
|
|||
else:
|
||||
assert MockCaptureManager.calls == []
|
||||
assert out_file.getvalue() == "\nsome message\n"
|
||||
|
||||
|
||||
def test_collection_live_logging(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
logging.getLogger().info("Normal message")
|
||||
"""
|
||||
)
|
||||
|
||||
result = testdir.runpytest("--log-cli-level=INFO")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"collecting*",
|
||||
"*--- live log collection ---*",
|
||||
"*Normal message*",
|
||||
"collected 0 items",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_collection_logging_to_file(testdir):
|
||||
log_file = testdir.tmpdir.join("pytest.log").strpath
|
||||
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
log_file={}
|
||||
log_file_level = INFO
|
||||
""".format(
|
||||
log_file
|
||||
)
|
||||
)
|
||||
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
logging.getLogger().info("Normal message")
|
||||
|
||||
def test_simple():
|
||||
logging.getLogger().debug("debug message in test_simple")
|
||||
logging.getLogger().info("info message in test_simple")
|
||||
"""
|
||||
)
|
||||
|
||||
result = testdir.runpytest()
|
||||
|
||||
assert "--- live log collection ---" not in result.stdout.str()
|
||||
|
||||
assert result.ret == 0
|
||||
assert os.path.isfile(log_file)
|
||||
with open(log_file, encoding="utf-8") as rfh:
|
||||
contents = rfh.read()
|
||||
assert "Normal message" in contents
|
||||
assert "debug message in test_simple" not in contents
|
||||
assert "info message in test_simple" in contents
|
||||
|
|
|
@ -240,6 +240,9 @@ class TestClass(object):
|
|||
assert result.ret == EXIT_NOTESTSCOLLECTED
|
||||
|
||||
|
||||
@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(
|
||||
|
@ -1255,6 +1258,9 @@ class TestReportInfo(object):
|
|||
assert lineno == 1
|
||||
assert msg == "TestClass"
|
||||
|
||||
@pytest.mark.filterwarnings(
|
||||
"ignore:usage of Generator.Function is deprecated, please use pytest.Function instead"
|
||||
)
|
||||
def test_generator_reportinfo(self, testdir):
|
||||
modcol = testdir.getmodulecol(
|
||||
"""
|
||||
|
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
from _pytest.pytester import get_public_names
|
||||
from _pytest.fixtures import FixtureLookupError, FixtureRequest
|
||||
from _pytest import fixtures
|
||||
from _pytest.compat import Path
|
||||
from _pytest.pathlib import Path
|
||||
|
||||
|
||||
def test_getfuncargnames():
|
||||
|
@ -993,6 +993,7 @@ class TestRequestCachedSetup(object):
|
|||
)
|
||||
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)
|
||||
|
@ -1010,6 +1011,7 @@ class TestRequestCachedSetup(object):
|
|||
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)
|
||||
|
@ -1221,8 +1223,7 @@ class TestFixtureUsages(object):
|
|||
result = testdir.runpytest_inprocess()
|
||||
result.stdout.fnmatch_lines(
|
||||
(
|
||||
"*ValueError: fixture badscope from test_invalid_scope.py has an unsupported"
|
||||
" scope value 'functions'"
|
||||
"*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'"
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -3611,16 +3612,15 @@ class TestParameterizedSubRequest(object):
|
|||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
"""
|
||||
E*Failed: The requested fixture has no parameter defined for test:
|
||||
E* test_call_from_fixture.py::test_foo
|
||||
E*
|
||||
E*Requested fixture 'fix_with_param' defined in:
|
||||
E*test_call_from_fixture.py:4
|
||||
E*Requested here:
|
||||
E*test_call_from_fixture.py:9
|
||||
*1 error*
|
||||
"""
|
||||
[
|
||||
"The requested fixture has no parameter defined for test:",
|
||||
" test_call_from_fixture.py::test_foo",
|
||||
"Requested fixture 'fix_with_param' defined in:",
|
||||
"test_call_from_fixture.py:4",
|
||||
"Requested here:",
|
||||
"test_call_from_fixture.py:9",
|
||||
"*1 error in*",
|
||||
]
|
||||
)
|
||||
|
||||
def test_call_from_test(self, testdir):
|
||||
|
@ -3638,16 +3638,15 @@ class TestParameterizedSubRequest(object):
|
|||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
"""
|
||||
E*Failed: The requested fixture has no parameter defined for test:
|
||||
E* test_call_from_test.py::test_foo
|
||||
E*
|
||||
E*Requested fixture 'fix_with_param' defined in:
|
||||
E*test_call_from_test.py:4
|
||||
E*Requested here:
|
||||
E*test_call_from_test.py:8
|
||||
*1 failed*
|
||||
"""
|
||||
[
|
||||
"The requested fixture has no parameter defined for test:",
|
||||
" test_call_from_test.py::test_foo",
|
||||
"Requested fixture 'fix_with_param' defined in:",
|
||||
"test_call_from_test.py:4",
|
||||
"Requested here:",
|
||||
"test_call_from_test.py:8",
|
||||
"*1 failed*",
|
||||
]
|
||||
)
|
||||
|
||||
def test_external_fixture(self, testdir):
|
||||
|
@ -3669,16 +3668,16 @@ class TestParameterizedSubRequest(object):
|
|||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
"""
|
||||
E*Failed: The requested fixture has no parameter defined for test:
|
||||
E* test_external_fixture.py::test_foo
|
||||
E*
|
||||
E*Requested fixture 'fix_with_param' defined in:
|
||||
E*conftest.py:4
|
||||
E*Requested here:
|
||||
E*test_external_fixture.py:2
|
||||
*1 failed*
|
||||
"""
|
||||
[
|
||||
"The requested fixture has no parameter defined for test:",
|
||||
" test_external_fixture.py::test_foo",
|
||||
"",
|
||||
"Requested fixture 'fix_with_param' defined in:",
|
||||
"conftest.py:4",
|
||||
"Requested here:",
|
||||
"test_external_fixture.py:2",
|
||||
"*1 failed*",
|
||||
]
|
||||
)
|
||||
|
||||
def test_non_relative_path(self, testdir):
|
||||
|
@ -3713,16 +3712,16 @@ class TestParameterizedSubRequest(object):
|
|||
testdir.syspathinsert(fixdir)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
"""
|
||||
E*Failed: The requested fixture has no parameter defined for test:
|
||||
E* test_foos.py::test_foo
|
||||
E*
|
||||
E*Requested fixture 'fix_with_param' defined in:
|
||||
E*fix.py:4
|
||||
E*Requested here:
|
||||
E*test_foos.py:4
|
||||
*1 failed*
|
||||
"""
|
||||
[
|
||||
"The requested fixture has no parameter defined for test:",
|
||||
" test_foos.py::test_foo",
|
||||
"",
|
||||
"Requested fixture 'fix_with_param' defined in:",
|
||||
"*fix.py:4",
|
||||
"Requested here:",
|
||||
"test_foos.py:4",
|
||||
"*1 failed*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -127,10 +127,11 @@ class TestMetafunc(object):
|
|||
pass
|
||||
|
||||
metafunc = self.Metafunc(func)
|
||||
try:
|
||||
with pytest.raises(
|
||||
pytest.fail.Exception,
|
||||
match=r"parametrize\(\) call in func got an unexpected scope value 'doggy'",
|
||||
):
|
||||
metafunc.parametrize("x", [1], scope="doggy")
|
||||
except ValueError as ve:
|
||||
assert "has an unsupported scope value 'doggy'" in str(ve)
|
||||
|
||||
def test_find_parametrized_scope(self):
|
||||
"""unittest for _find_parametrized_scope (#3941)"""
|
||||
|
@ -206,15 +207,12 @@ class TestMetafunc(object):
|
|||
|
||||
metafunc = self.Metafunc(func)
|
||||
|
||||
pytest.raises(
|
||||
ValueError, lambda: metafunc.parametrize("x", [1, 2], ids=["basic"])
|
||||
)
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
metafunc.parametrize("x", [1, 2], ids=["basic"])
|
||||
|
||||
pytest.raises(
|
||||
ValueError,
|
||||
lambda: metafunc.parametrize(
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
metafunc.parametrize(
|
||||
("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"]
|
||||
),
|
||||
)
|
||||
|
||||
@pytest.mark.issue510
|
||||
|
@ -573,7 +571,7 @@ class TestMetafunc(object):
|
|||
pass
|
||||
|
||||
metafunc = self.Metafunc(func)
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"])
|
||||
|
||||
@pytest.mark.issue714
|
||||
|
@ -1189,7 +1187,9 @@ class TestMetafuncFunctional(object):
|
|||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
["*ids must be list of strings, found: 2 (type: int)*"]
|
||||
[
|
||||
"*In test_ids_numbers: ids must be list of strings, found: 2 (type: *'int'>)*"
|
||||
]
|
||||
)
|
||||
|
||||
def test_parametrize_with_identical_ids_get_unique_names(self, testdir):
|
||||
|
@ -1326,13 +1326,13 @@ class TestMetafuncFunctional(object):
|
|||
attr
|
||||
)
|
||||
)
|
||||
reprec = testdir.inline_run("--collectonly")
|
||||
failures = reprec.getfailures()
|
||||
assert len(failures) == 1
|
||||
expectederror = "MarkerError: test_foo has '{}', spelling should be 'parametrize'".format(
|
||||
attr
|
||||
result = testdir.runpytest("--collectonly")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"test_foo has '{}' mark, spelling should be 'parametrize'".format(attr),
|
||||
"*1 error in*",
|
||||
]
|
||||
)
|
||||
assert expectederror in failures[0].longrepr.reprcrash.message
|
||||
|
||||
|
||||
class TestMetafuncFunctionalAuto(object):
|
||||
|
|
|
@ -4,7 +4,7 @@ import textwrap
|
|||
import py
|
||||
import pytest
|
||||
from _pytest.config import PytestPluginManager
|
||||
from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR
|
||||
from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_OK, EXIT_USAGEERROR
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=["global", "inpackage"])
|
||||
|
@ -186,6 +186,52 @@ def test_conftest_confcutdir(testdir):
|
|||
assert "warning: could not load initial" not in result.stdout.str()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not hasattr(py.path.local, "mksymlinkto"),
|
||||
reason="symlink not available on this platform",
|
||||
)
|
||||
def test_conftest_symlink(testdir):
|
||||
"""Ensure that conftest.py is used for resolved symlinks."""
|
||||
realtests = testdir.tmpdir.mkdir("real").mkdir("app").mkdir("tests")
|
||||
testdir.tmpdir.join("symlinktests").mksymlinkto(realtests)
|
||||
testdir.makepyfile(
|
||||
**{
|
||||
"real/app/tests/test_foo.py": "def test1(fixture): pass",
|
||||
"real/conftest.py": textwrap.dedent(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
print("conftest_loaded")
|
||||
|
||||
@pytest.fixture
|
||||
def fixture():
|
||||
print("fixture_used")
|
||||
"""
|
||||
),
|
||||
}
|
||||
)
|
||||
result = testdir.runpytest("-vs", "symlinktests")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*conftest_loaded*",
|
||||
"real/app/tests/test_foo.py::test1 fixture_used",
|
||||
"PASSED",
|
||||
]
|
||||
)
|
||||
assert result.ret == EXIT_OK
|
||||
|
||||
realtests.ensure("__init__.py")
|
||||
result = testdir.runpytest("-vs", "symlinktests/test_foo.py::test1")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*conftest_loaded*",
|
||||
"real/app/tests/test_foo.py::test1 fixture_used",
|
||||
"PASSED",
|
||||
]
|
||||
)
|
||||
assert result.ret == EXIT_OK
|
||||
|
||||
|
||||
def test_no_conftest(testdir):
|
||||
testdir.makeconftest("assert 0")
|
||||
result = testdir.runpytest("--noconftest")
|
||||
|
|
|
@ -13,7 +13,7 @@ from _pytest.mark import (
|
|||
transfer_markers,
|
||||
EMPTY_PARAMETERSET_OPTION,
|
||||
)
|
||||
from _pytest.nodes import Node
|
||||
from _pytest.nodes import Node, Collector
|
||||
|
||||
ignore_markinfo = pytest.mark.filterwarnings(
|
||||
"ignore:MarkInfo objects:pytest.RemovedInPytest4Warning"
|
||||
|
@ -247,7 +247,7 @@ def test_marker_without_description(testdir):
|
|||
)
|
||||
ftdir = testdir.mkdir("ft1_dummy")
|
||||
testdir.tmpdir.join("conftest.py").move(ftdir.join("conftest.py"))
|
||||
rec = testdir.runpytest_subprocess("--strict")
|
||||
rec = testdir.runpytest("--strict")
|
||||
rec.assert_outcomes()
|
||||
|
||||
|
||||
|
@ -302,7 +302,7 @@ def test_strict_prohibits_unregistered_markers(testdir):
|
|||
)
|
||||
result = testdir.runpytest("--strict")
|
||||
assert result.ret != 0
|
||||
result.stdout.fnmatch_lines(["*unregisteredmark*not*registered*"])
|
||||
result.stdout.fnmatch_lines(["'unregisteredmark' not a registered marker"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -1103,7 +1103,14 @@ class TestMarkDecorator(object):
|
|||
@pytest.mark.parametrize("mark", [None, "", "skip", "xfail"])
|
||||
def test_parameterset_for_parametrize_marks(testdir, mark):
|
||||
if mark is not None:
|
||||
testdir.makeini("[pytest]\n{}={}".format(EMPTY_PARAMETERSET_OPTION, mark))
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
{}={}
|
||||
""".format(
|
||||
EMPTY_PARAMETERSET_OPTION, mark
|
||||
)
|
||||
)
|
||||
|
||||
config = testdir.parseconfig()
|
||||
from _pytest.mark import pytest_configure, get_empty_parameterset_mark
|
||||
|
@ -1119,6 +1126,34 @@ def test_parameterset_for_parametrize_marks(testdir, mark):
|
|||
assert result_mark.kwargs.get("run") is False
|
||||
|
||||
|
||||
def test_parameterset_for_fail_at_collect(testdir):
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
{}=fail_at_collect
|
||||
""".format(
|
||||
EMPTY_PARAMETERSET_OPTION
|
||||
)
|
||||
)
|
||||
|
||||
config = testdir.parseconfig()
|
||||
from _pytest.mark import pytest_configure, get_empty_parameterset_mark
|
||||
from _pytest.compat import getfslineno
|
||||
|
||||
pytest_configure(config)
|
||||
|
||||
test_func = all
|
||||
func_name = test_func.__name__
|
||||
_, func_lineno = getfslineno(test_func)
|
||||
expected_errmsg = r"Empty parameter set in '%s' at line %d" % (
|
||||
func_name,
|
||||
func_lineno,
|
||||
)
|
||||
|
||||
with pytest.raises(Collector.CollectError, match=expected_errmsg):
|
||||
get_empty_parameterset_mark(config, ["a"], test_func)
|
||||
|
||||
|
||||
def test_parameterset_for_parametrize_bad_markname(testdir):
|
||||
with pytest.raises(pytest.UsageError):
|
||||
test_parameterset_for_parametrize_marks(testdir, "bad")
|
||||
|
|
|
@ -4,7 +4,7 @@ import py
|
|||
|
||||
import pytest
|
||||
|
||||
from _pytest.paths import fnmatch_ex
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
|
||||
|
||||
class TestPort:
|
||||
|
|
|
@ -4,6 +4,7 @@ import os
|
|||
import py.path
|
||||
import pytest
|
||||
import sys
|
||||
import time
|
||||
import _pytest.pytester as pytester
|
||||
from _pytest.pytester import HookRecorder
|
||||
from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot
|
||||
|
@ -401,3 +402,34 @@ def test_testdir_subprocess(testdir):
|
|||
def test_unicode_args(testdir):
|
||||
result = testdir.runpytest("-k", u"💩")
|
||||
assert result.ret == EXIT_NOTESTSCOLLECTED
|
||||
|
||||
|
||||
def test_testdir_run_no_timeout(testdir):
|
||||
testfile = testdir.makepyfile("def test_no_timeout(): pass")
|
||||
assert testdir.runpytest_subprocess(testfile).ret == EXIT_OK
|
||||
|
||||
|
||||
def test_testdir_run_with_timeout(testdir):
|
||||
testfile = testdir.makepyfile("def test_no_timeout(): pass")
|
||||
|
||||
timeout = 120
|
||||
|
||||
start = time.time()
|
||||
result = testdir.runpytest_subprocess(testfile, timeout=timeout)
|
||||
end = time.time()
|
||||
duration = end - start
|
||||
|
||||
assert result.ret == EXIT_OK
|
||||
assert duration < timeout
|
||||
|
||||
|
||||
def test_testdir_run_timeout_expires(testdir):
|
||||
testfile = testdir.makepyfile(
|
||||
"""
|
||||
import time
|
||||
|
||||
def test_timeout():
|
||||
time.sleep(10)"""
|
||||
)
|
||||
with pytest.raises(testdir.TimeoutExpired):
|
||||
testdir.runpytest_subprocess(testfile, timeout=1)
|
||||
|
|
|
@ -76,9 +76,8 @@ class TestDeprecatedCall(object):
|
|||
)
|
||||
|
||||
def test_deprecated_call_raises(self):
|
||||
with pytest.raises(AssertionError) as excinfo:
|
||||
with pytest.raises(pytest.fail.Exception, match="No warnings of type"):
|
||||
pytest.deprecated_call(self.dep, 3, 5)
|
||||
assert "Did not produce" in str(excinfo)
|
||||
|
||||
def test_deprecated_call(self):
|
||||
pytest.deprecated_call(self.dep, 0, 5)
|
||||
|
@ -100,7 +99,7 @@ class TestDeprecatedCall(object):
|
|||
assert warn_explicit is warnings.warn_explicit
|
||||
|
||||
def test_deprecated_explicit_call_raises(self):
|
||||
with pytest.raises(AssertionError):
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
pytest.deprecated_call(self.dep_explicit, 3)
|
||||
|
||||
def test_deprecated_explicit_call(self):
|
||||
|
@ -116,8 +115,8 @@ class TestDeprecatedCall(object):
|
|||
def f():
|
||||
pass
|
||||
|
||||
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
|
||||
with pytest.raises(AssertionError, match=msg):
|
||||
msg = "No warnings of type (.*DeprecationWarning.*, .*PendingDeprecationWarning.*)"
|
||||
with pytest.raises(pytest.fail.Exception, match=msg):
|
||||
if mode == "call":
|
||||
pytest.deprecated_call(f)
|
||||
else:
|
||||
|
@ -179,12 +178,20 @@ class TestDeprecatedCall(object):
|
|||
def f():
|
||||
warnings.warn(warning("hi"))
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
pytest.deprecated_call(f)
|
||||
with pytest.raises(AssertionError):
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
with pytest.deprecated_call():
|
||||
f()
|
||||
|
||||
def test_deprecated_call_supports_match(self):
|
||||
with pytest.deprecated_call(match=r"must be \d+$"):
|
||||
warnings.warn("value must be 42", DeprecationWarning)
|
||||
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
with pytest.deprecated_call(match=r"must be \d+$"):
|
||||
warnings.warn("this is not here", DeprecationWarning)
|
||||
|
||||
|
||||
class TestWarns(object):
|
||||
def test_strings(self):
|
||||
|
@ -343,3 +350,13 @@ class TestWarns(object):
|
|||
with pytest.warns(UserWarning, match=r"aaa"):
|
||||
warnings.warn("bbbbbbbbbb", UserWarning)
|
||||
warnings.warn("cccccccccc", UserWarning)
|
||||
|
||||
@pytest.mark.filterwarnings("ignore")
|
||||
def test_can_capture_previously_warned(self):
|
||||
def f():
|
||||
warnings.warn(UserWarning("ohai"))
|
||||
return 10
|
||||
|
||||
assert f() == 10
|
||||
assert pytest.warns(UserWarning, f) == 10
|
||||
assert pytest.warns(UserWarning, f) == 10
|
||||
|
|
|
@ -570,7 +570,20 @@ def test_pytest_exit_msg(testdir):
|
|||
result.stderr.fnmatch_lines(["Exit: oh noes"])
|
||||
|
||||
|
||||
def test_pytest_fail_notrace(testdir):
|
||||
def test_pytest_exit_returncode(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
def test_foo():
|
||||
pytest.exit("some exit msg", 99)
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == 99
|
||||
|
||||
|
||||
def test_pytest_fail_notrace_runtest(testdir):
|
||||
"""Test pytest.fail(..., pytrace=False) does not show tracebacks during test run."""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
@ -585,6 +598,21 @@ def test_pytest_fail_notrace(testdir):
|
|||
assert "def teardown_function" not in result.stdout.str()
|
||||
|
||||
|
||||
def test_pytest_fail_notrace_collection(testdir):
|
||||
"""Test pytest.fail(..., pytrace=False) does not show tracebacks during collection."""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
def some_internal_function():
|
||||
pytest.fail("hello", pytrace=False)
|
||||
some_internal_function()
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(["hello"])
|
||||
assert "def some_internal_function()" not in result.stdout.str()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("str_prefix", ["u", ""])
|
||||
def test_pytest_fail_notrace_non_ascii(testdir, str_prefix):
|
||||
"""Fix pytest.fail with pytrace=False with non-ascii characters (#1178).
|
||||
|
|
|
@ -154,7 +154,7 @@ class TestTerminal(object):
|
|||
)
|
||||
result = testdir.runpytest(p2)
|
||||
result.stdout.fnmatch_lines(["*test_p2.py .*", "*1 passed*"])
|
||||
result = testdir.runpytest("-v", p2)
|
||||
result = testdir.runpytest("-vv", p2)
|
||||
result.stdout.fnmatch_lines(
|
||||
["*test_p2.py::TestMore::test_p1* <- *test_p1.py*PASSED*"]
|
||||
)
|
||||
|
@ -170,7 +170,7 @@ class TestTerminal(object):
|
|||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest("-v")
|
||||
result = testdir.runpytest("-vv")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"])
|
||||
assert " <- " not in result.stdout.str()
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
from __future__ import absolute_import, division, print_function
|
||||
import sys
|
||||
import py
|
||||
|
||||
import six
|
||||
|
||||
import pytest
|
||||
from _pytest.pathlib import Path
|
||||
|
||||
|
||||
def test_tmpdir_fixture(testdir):
|
||||
|
@ -19,11 +22,11 @@ def test_ensuretemp(recwarn):
|
|||
|
||||
class TestTempdirHandler(object):
|
||||
def test_mktemp(self, testdir):
|
||||
from _pytest.tmpdir import TempdirFactory
|
||||
from _pytest.tmpdir import TempdirFactory, TempPathFactory
|
||||
|
||||
config = testdir.parseconfig()
|
||||
config.option.basetemp = testdir.mkdir("hello")
|
||||
t = TempdirFactory(config)
|
||||
t = TempdirFactory(TempPathFactory.from_config(config))
|
||||
tmp = t.mktemp("world")
|
||||
assert tmp.relto(t.getbasetemp()) == "world0"
|
||||
tmp = t.mktemp("this")
|
||||
|
@ -65,10 +68,6 @@ def test_basetemp(testdir):
|
|||
assert mytemp.join("hello").check()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not hasattr(py.path.local, "mksymlinkto"),
|
||||
reason="symlink not available on this platform",
|
||||
)
|
||||
def test_tmpdir_always_is_realpath(testdir):
|
||||
# the reason why tmpdir should be a realpath is that
|
||||
# when you cd to it and do "os.getcwd()" you will anyway
|
||||
|
@ -78,7 +77,7 @@ def test_tmpdir_always_is_realpath(testdir):
|
|||
# os.environ["PWD"]
|
||||
realtemp = testdir.tmpdir.mkdir("myrealtemp")
|
||||
linktemp = testdir.tmpdir.join("symlinktemp")
|
||||
linktemp.mksymlinkto(realtemp)
|
||||
attempt_symlink_to(linktemp, str(realtemp))
|
||||
p = testdir.makepyfile(
|
||||
"""
|
||||
def test_1(tmpdir):
|
||||
|
@ -111,7 +110,7 @@ def test_tmpdir_factory(testdir):
|
|||
def session_dir(tmpdir_factory):
|
||||
return tmpdir_factory.mktemp('data', numbered=False)
|
||||
def test_some(session_dir):
|
||||
session_dir.isdir()
|
||||
assert session_dir.isdir()
|
||||
"""
|
||||
)
|
||||
reprec = testdir.inline_run()
|
||||
|
@ -184,3 +183,113 @@ def test_get_user(monkeypatch):
|
|||
monkeypatch.delenv("USER", raising=False)
|
||||
monkeypatch.delenv("USERNAME", raising=False)
|
||||
assert get_user() is None
|
||||
|
||||
|
||||
class TestNumberedDir(object):
|
||||
PREFIX = "fun-"
|
||||
|
||||
def test_make(self, tmp_path):
|
||||
from _pytest.pathlib import make_numbered_dir
|
||||
|
||||
for i in range(10):
|
||||
d = make_numbered_dir(root=tmp_path, prefix=self.PREFIX)
|
||||
assert d.name.startswith(self.PREFIX)
|
||||
assert d.name.endswith(str(i))
|
||||
|
||||
def test_cleanup_lock_create(self, tmp_path):
|
||||
d = tmp_path.joinpath("test")
|
||||
d.mkdir()
|
||||
from _pytest.pathlib import create_cleanup_lock
|
||||
|
||||
lockfile = create_cleanup_lock(d)
|
||||
with pytest.raises(EnvironmentError, match="cannot create lockfile in .*"):
|
||||
create_cleanup_lock(d)
|
||||
|
||||
lockfile.unlink()
|
||||
|
||||
def test_lock_register_cleanup_removal(self, tmp_path):
|
||||
from _pytest.pathlib import create_cleanup_lock, register_cleanup_lock_removal
|
||||
|
||||
lock = create_cleanup_lock(tmp_path)
|
||||
|
||||
registry = []
|
||||
register_cleanup_lock_removal(lock, register=registry.append)
|
||||
|
||||
cleanup_func, = registry
|
||||
|
||||
assert lock.is_file()
|
||||
|
||||
cleanup_func(original_pid="intentionally_different")
|
||||
|
||||
assert lock.is_file()
|
||||
|
||||
cleanup_func()
|
||||
|
||||
assert not lock.exists()
|
||||
|
||||
cleanup_func()
|
||||
|
||||
assert not lock.exists()
|
||||
|
||||
def _do_cleanup(self, tmp_path):
|
||||
self.test_make(tmp_path)
|
||||
from _pytest.pathlib import cleanup_numbered_dir
|
||||
|
||||
cleanup_numbered_dir(
|
||||
root=tmp_path,
|
||||
prefix=self.PREFIX,
|
||||
keep=2,
|
||||
consider_lock_dead_if_created_before=0,
|
||||
)
|
||||
|
||||
def test_cleanup_keep(self, tmp_path):
|
||||
self._do_cleanup(tmp_path)
|
||||
a, b = tmp_path.iterdir()
|
||||
print(a, b)
|
||||
|
||||
def test_cleanup_locked(self, tmp_path):
|
||||
|
||||
from _pytest import pathlib
|
||||
|
||||
p = pathlib.make_numbered_dir(root=tmp_path, prefix=self.PREFIX)
|
||||
|
||||
pathlib.create_cleanup_lock(p)
|
||||
|
||||
assert not pathlib.ensure_deletable(
|
||||
p, consider_lock_dead_if_created_before=p.stat().st_mtime - 1
|
||||
)
|
||||
assert pathlib.ensure_deletable(
|
||||
p, consider_lock_dead_if_created_before=p.stat().st_mtime + 1
|
||||
)
|
||||
|
||||
def test_rmtree(self, tmp_path):
|
||||
from _pytest.pathlib import rmtree
|
||||
|
||||
adir = tmp_path / "adir"
|
||||
adir.mkdir()
|
||||
rmtree(adir)
|
||||
|
||||
assert not adir.exists()
|
||||
|
||||
adir.mkdir()
|
||||
afile = adir / "afile"
|
||||
afile.write_bytes(b"aa")
|
||||
|
||||
rmtree(adir, force=True)
|
||||
assert not adir.exists()
|
||||
|
||||
def test_cleanup_symlink(self, tmp_path):
|
||||
the_symlink = tmp_path / (self.PREFIX + "current")
|
||||
attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5"))
|
||||
self._do_cleanup(tmp_path)
|
||||
|
||||
|
||||
def attempt_symlink_to(path, to_path):
|
||||
"""Try to make a symlink from "path" to "to_path", skipping in case this platform
|
||||
does not support it or we don't have sufficient privileges (common on Windows)."""
|
||||
if sys.platform.startswith("win") and six.PY2:
|
||||
pytest.skip("pathlib for some reason cannot make symlinks on Python 2")
|
||||
try:
|
||||
Path(path).symlink_to(Path(to_path))
|
||||
except OSError:
|
||||
pytest.skip("could not create symbolic link")
|
||||
|
|
|
@ -430,6 +430,50 @@ def test_hide_pytest_internal_warnings(testdir, ignore_pytest_warnings):
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_on_cmdline", [True, False])
|
||||
def test_option_precedence_cmdline_over_ini(testdir, ignore_on_cmdline):
|
||||
"""filters defined in the command-line should take precedence over filters in ini files (#3946)."""
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
filterwarnings = error
|
||||
"""
|
||||
)
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import warnings
|
||||
def test():
|
||||
warnings.warn(UserWarning('hello'))
|
||||
"""
|
||||
)
|
||||
args = ["-W", "ignore"] if ignore_on_cmdline else []
|
||||
result = testdir.runpytest(*args)
|
||||
if ignore_on_cmdline:
|
||||
result.stdout.fnmatch_lines(["* 1 passed in*"])
|
||||
else:
|
||||
result.stdout.fnmatch_lines(["* 1 failed in*"])
|
||||
|
||||
|
||||
def test_option_precedence_mark(testdir):
|
||||
"""Filters defined by marks should always take precedence (#3946)."""
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
filterwarnings = ignore
|
||||
"""
|
||||
)
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import pytest, warnings
|
||||
@pytest.mark.filterwarnings('error')
|
||||
def test():
|
||||
warnings.warn(UserWarning('hello'))
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("-W", "ignore")
|
||||
result.stdout.fnmatch_lines(["* 1 failed in*"])
|
||||
|
||||
|
||||
class TestDeprecationWarningsByDefault:
|
||||
"""
|
||||
Note: all pytest runs are executed in a subprocess so we don't inherit warning filters
|
||||
|
@ -451,8 +495,18 @@ class TestDeprecationWarningsByDefault:
|
|||
)
|
||||
)
|
||||
|
||||
def test_shown_by_default(self, testdir):
|
||||
@pytest.mark.parametrize("customize_filters", [True, False])
|
||||
def test_shown_by_default(self, testdir, customize_filters):
|
||||
"""Show deprecation warnings by default, even if user has customized the warnings filters (#4013)."""
|
||||
self.create_file(testdir)
|
||||
if customize_filters:
|
||||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
once::UserWarning
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest_subprocess()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
|
@ -468,7 +522,9 @@ class TestDeprecationWarningsByDefault:
|
|||
testdir.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
filterwarnings = once::UserWarning
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
ignore::PendingDeprecationWarning
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest_subprocess()
|
||||
|
@ -479,7 +535,8 @@ class TestDeprecationWarningsByDefault:
|
|||
be displayed normally.
|
||||
"""
|
||||
self.create_file(
|
||||
testdir, mark='@pytest.mark.filterwarnings("once::UserWarning")'
|
||||
testdir,
|
||||
mark='@pytest.mark.filterwarnings("ignore::PendingDeprecationWarning")',
|
||||
)
|
||||
result = testdir.runpytest_subprocess()
|
||||
result.stdout.fnmatch_lines(
|
||||
|
@ -492,7 +549,12 @@ class TestDeprecationWarningsByDefault:
|
|||
|
||||
def test_hidden_by_cmdline(self, testdir):
|
||||
self.create_file(testdir)
|
||||
result = testdir.runpytest_subprocess("-W", "once::UserWarning")
|
||||
result = testdir.runpytest_subprocess(
|
||||
"-W",
|
||||
"ignore::DeprecationWarning",
|
||||
"-W",
|
||||
"ignore::PendingDeprecationWarning",
|
||||
)
|
||||
assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
|
||||
|
||||
def test_hidden_by_system(self, testdir, monkeypatch):
|
||||
|
|
9
tox.ini
9
tox.ini
|
@ -194,13 +194,14 @@ commands = python scripts/release.py {posargs}
|
|||
|
||||
[pytest]
|
||||
minversion = 2.0
|
||||
plugins = pytester
|
||||
addopts = -ra -p pytester --ignore=testing/cx_freeze
|
||||
rsyncdirs = tox.ini pytest.py _pytest testing
|
||||
addopts = -ra -p pytester
|
||||
rsyncdirs = tox.ini doc src testing
|
||||
python_files = test_*.py *_test.py testing/*/*.py
|
||||
python_classes = Test Acceptance
|
||||
python_functions = test
|
||||
norecursedirs = .tox ja .hg cx_freeze_source testing/example_scripts
|
||||
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
|
||||
testpaths = testing
|
||||
norecursedirs = testing/example_scripts
|
||||
xfail_strict=true
|
||||
filterwarnings =
|
||||
error
|
||||
|
|
Loading…
Reference in New Issue