Merge remote-tracking branch 'upstream/features' into merge-features-into-master

This commit is contained in:
Bruno Oliveira 2018-10-16 17:41:27 -03:00
commit ee0a306ee4
71 changed files with 1897 additions and 597 deletions

View File

@ -20,7 +20,9 @@ repos:
- id: check-yaml - id: check-yaml
- id: debug-statements - id: debug-statements
exclude: _pytest/debugging.py exclude: _pytest/debugging.py
language_version: python3
- id: flake8 - id: flake8
language_version: python3
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v1.8.0 rev: v1.8.0
hooks: hooks:
@ -41,6 +43,6 @@ repos:
- id: changelogs-rst - id: changelogs-rst
name: changelog filenames name: changelog filenames
language: fail language: fail
entry: 'changelog files must be named ####.(feature|bugfix|doc|removal|vendor|trivial).rst' entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst'
exclude: changelog/(\d+\.(feature|bugfix|doc|removal|vendor|trivial).rst|README.rst|_template.rst) exclude: changelog/(\d+\.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst)
files: ^changelog/ files: ^changelog/

View File

@ -14,6 +14,7 @@ Allan Feldman
Anatoly Bubenkoff Anatoly Bubenkoff
Anders Hovmöller Anders Hovmöller
Andras Tim Andras Tim
Andrea Cimatoribus
Andreas Zeidler Andreas Zeidler
Andrzej Ostrowski Andrzej Ostrowski
Andy Freeland Andy Freeland
@ -120,6 +121,7 @@ Katerina Koukiou
Kevin Cox Kevin Cox
Kodi B. Arfer Kodi B. Arfer
Kostis Anagnostopoulos Kostis Anagnostopoulos
Kyle Altendorf
Lawrence Mitchell Lawrence Mitchell
Lee Kamentsky Lee Kamentsky
Lev Maximov Lev Maximov

View File

@ -18,6 +18,163 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. 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) pytest 3.8.2 (2018-10-02)
========================= =========================

View File

@ -1 +0,0 @@
Improve error message when test functions of ``unittest.TestCase`` subclasses use a parametrized fixture.

View File

@ -1 +0,0 @@
``request.fixturenames`` now correctly returns the name of fixtures created by ``request.getfixturevalue()``.

View File

@ -1 +0,0 @@
Update usefixtures documentation to clarify that it can't be used with fixture functions.

View File

@ -1 +0,0 @@
Update fixture documentation to specify that a fixture can be invoked twice in the scope it's defined for.

View File

@ -1 +0,0 @@
Exclude 0.00 second entries from ``--duration`` output unless ``-vv`` is passed on the command-line.

View File

@ -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.

View File

@ -1 +0,0 @@
Fix source reindenting by using ``textwrap.dedent`` directly.

View File

@ -1 +0,0 @@
Fixed formatting of string literals in internal tests.

View File

@ -1 +0,0 @@
Fix duplicate printing of internal errors when using ``--pdb``.

View File

@ -1 +0,0 @@
Add tempir testing example to CONTRIBUTING.rst guide

View File

@ -1 +0,0 @@
Display the filename when encountering ``SyntaxWarning``.

View File

@ -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. * ``feature``: new user facing features, like new command-line options and new behavior.
* ``bugfix``: fixes a reported bug. * ``bugfix``: fixes a reported bug.
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs. * ``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. * ``vendor``: changes in packages vendored in pytest.
* ``trivial``: fixing a small typo or internal change that might be noteworthy. * ``trivial``: fixing a small typo or internal change that might be noteworthy.

View File

@ -6,6 +6,8 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-3.9.1
release-3.9.0
release-3.8.2 release-3.8.2
release-3.8.1 release-3.8.1
release-3.8.0 release-3.8.0

View File

@ -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

View File

@ -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

View File

@ -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 See http://docs.python.org/library/warnings.html for information
on warning categories. on warning categories.
tmpdir_factory 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 tmpdir
Return a temporary directory path object Return a temporary directory path object
which is unique to each test function invocation, 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. path object.
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html .. _`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 no tests ran in 0.12 seconds

View File

@ -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 :class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
:ref:`standard warning filters <warnings>`. :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`` ``Config.warn`` and ``Node.warn``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -574,7 +574,7 @@ We can run this::
file $REGENDOC_TMPDIR/b/test_error.py, line 1 file $REGENDOC_TMPDIR/b/test_error.py, line 1
def test_root(db): # no db here, will error out def test_root(db): # no db here, will error out
E fixture 'db' not found 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. > use 'pytest --fixtures [testpath]' for help on them.
$REGENDOC_TMPDIR/b/test_error.py:1 $REGENDOC_TMPDIR/b/test_error.py:1

View File

@ -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. This is no longer needed and one can use the native ``import pdb;pdb.set_trace()`` call directly.
For more details see :ref:`breakpoints`. 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.

View File

@ -982,6 +982,7 @@ passed multiple times. The expected format is ``name=value``. For example::
* ``skip`` skips tests with an empty parameterset (default) * ``skip`` skips tests with an empty parameterset (default)
* ``xfail`` marks tests with an empty parameterset as xfail(run=False) * ``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 .. code-block:: ini

View File

@ -5,6 +5,76 @@
Temporary directories and files 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 The 'tmpdir' fixture
-------------------- --------------------

View File

@ -101,22 +101,28 @@ DeprecationWarning and PendingDeprecationWarning
------------------------------------------------ ------------------------------------------------
.. versionadded:: 3.8 .. versionadded:: 3.8
.. versionchanged:: 3.9
By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` if no other warning filters By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning``.
are configured.
To disable showing ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings, you might define any warnings Sometimes it is useful to hide some specific deprecation warnings that happen in code that you have no control over
filter either in the command-line or in the ini file, or you can use: (such as third-party libraries), in which case you might use the standard warning filters options (ini or marks).
For example:
.. code-block:: ini .. code-block:: ini
[pytest] [pytest]
filterwarnings = filterwarnings =
ignore::DeprecationWarning ignore:.*U.*mode is deprecated:DeprecationWarning
ignore::PendingDeprecationWarning
.. note:: .. 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 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 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>`_ by calling ``warnings.simplefilter`` (see issue `#2430 <https://github.com/pytest-dev/pytest/issues/2430>`_

View File

@ -420,9 +420,21 @@ additionally it is possible to copy examples for a example folder before running
============================= warnings summary ============================= ============================= warnings summary =============================
$REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time $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") 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 -- 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 For more information about the result object that ``runpytest()`` returns, and
the methods that it provides please check out the :py:class:`RunResult the methods that it provides please check out the :py:class:`RunResult

View File

@ -1,6 +1,7 @@
[build-system] [build-system]
requires = [ requires = [
"setuptools", # sync with setup.py until we discard non-pep-517/518
"setuptools>=30.3",
"setuptools-scm", "setuptools-scm",
"wheel", "wheel",
] ]
@ -15,7 +16,12 @@ template = "changelog/_template.rst"
[[tool.towncrier.type]] [[tool.towncrier.type]]
directory = "removal" directory = "removal"
name = "Deprecations and Removals" name = "Removals"
showcontent = true
[[tool.towncrier.type]]
directory = "deprecation"
name = "Deprecations"
showcontent = true showcontent = true
[[tool.towncrier.type]] [[tool.towncrier.type]]

View File

@ -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] [build_sphinx]
source-dir = doc/en/ source-dir = doc/en/
build-dir = doc/build build-dir = doc/build
@ -13,8 +65,6 @@ universal = 1
ignore = ignore =
_pytest/_version.py _pytest/_version.py
[metadata]
license_file = LICENSE
[devpi:upload] [devpi:upload]
formats = sdist.tgz,bdist_wheel formats = sdist.tgz,bdist_wheel

130
setup.py
View File

@ -1,126 +1,34 @@
import os import os
import sys
import setuptools
import pkg_resources
from setuptools import setup from setuptools import setup
classifiers = [
"Development Status :: 6 - Mature", # TODO: if py gets upgrade to >=1.6,
"Intended Audience :: Developers", # remove _width_of_current_line in terminal.py
"License :: OSI Approved :: MIT License", INSTALL_REQUIRES = [
"Operating System :: POSIX", "py>=1.5.0",
"Operating System :: Microsoft :: Windows", "six>=1.10.0",
"Operating System :: MacOS :: MacOS X", "setuptools",
"Topic :: Software Development :: Testing", "attrs>=17.4.0",
"Topic :: Software Development :: Libraries", "more-itertools>=4.0.0",
"Topic :: Utilities", "atomicwrites>=1.0",
] + [ 'funcsigs;python_version<"3.0"',
("Programming Language :: Python :: %s" % x) 'pathlib2>=2.2.0;python_version<"3.6"',
for x in "2 2.7 3 3.4 3.5 3.6 3.7".split() 'colorama;sys_platform=="win32"',
] ]
with open("README.rst") as fd:
long_description = fd.read()
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
def get_environment_marker_support_level(): # used by tox.ini to test with pluggy master
""" if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ:
Tests how well setuptools supports PEP-426 environment marker. INSTALL_REQUIRES.append("pluggy>=0.7")
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(): 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
"six>=1.10.0",
"setuptools",
"attrs>=17.4.0",
"more-itertools>=4.0.0",
"atomicwrites>=1.0",
]
# 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")
setup( setup(
name="pytest",
description="pytest: simple powerful testing with Python",
long_description=long_description,
use_scm_version={"write_to": "src/_pytest/_version.py"}, use_scm_version={"write_to": "src/_pytest/_version.py"},
url="https://docs.pytest.org/en/latest/", setup_requires=["setuptools-scm", "setuptools>=30.3"],
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"],
package_dir={"": "src"}, package_dir={"": "src"},
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", install_requires=INSTALL_REQUIRES,
install_requires=install_requires,
extras_require=extras_require,
packages=[
"_pytest",
"_pytest.assertion",
"_pytest._code",
"_pytest.mark",
"_pytest.config",
],
py_modules=["pytest"],
zip_safe=False,
) )

View File

@ -4,6 +4,7 @@ from .code import Code # noqa
from .code import ExceptionInfo # noqa from .code import ExceptionInfo # noqa
from .code import Frame # noqa from .code import Frame # noqa
from .code import Traceback # noqa from .code import Traceback # noqa
from .code import filter_traceback # noqa
from .code import getrawcode # noqa from .code import getrawcode # noqa
from .source import Source # noqa from .source import Source # noqa
from .source import compile_ as compile # noqa from .source import compile_ as compile # noqa

View File

@ -6,8 +6,10 @@ import traceback
from inspect import CO_VARARGS, CO_VARKEYWORDS from inspect import CO_VARARGS, CO_VARKEYWORDS
import attr import attr
import pluggy
import re import re
from weakref import ref from weakref import ref
import _pytest
from _pytest.compat import _PY2, _PY3, PY35, safe_str from _pytest.compat import _PY2, _PY3, PY35, safe_str
from six import text_type from six import text_type
import py import py
@ -451,13 +453,35 @@ class ExceptionInfo(object):
tbfilter=True, tbfilter=True,
funcargs=False, funcargs=False,
truncate_locals=True, truncate_locals=True,
chain=True,
): ):
""" return str()able representation of this exception info. """
showlocals: show locals per traceback entry Return str()able representation of this exception info.
style: long|short|no|native traceback style
tbfilter: hide entries (where __tracebackhide__ is true)
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": if style == "native":
return ReprExceptionInfo( return ReprExceptionInfo(
@ -476,6 +500,7 @@ class ExceptionInfo(object):
tbfilter=tbfilter, tbfilter=tbfilter,
funcargs=funcargs, funcargs=funcargs,
truncate_locals=truncate_locals, truncate_locals=truncate_locals,
chain=chain,
) )
return fmt.repr_excinfo(self) return fmt.repr_excinfo(self)
@ -516,6 +541,7 @@ class FormattedExcinfo(object):
tbfilter = attr.ib(default=True) tbfilter = attr.ib(default=True)
funcargs = attr.ib(default=False) funcargs = attr.ib(default=False)
truncate_locals = attr.ib(default=True) truncate_locals = attr.ib(default=True)
chain = attr.ib(default=True)
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False) astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
def _getindent(self, source): def _getindent(self, source):
@ -735,7 +761,7 @@ class FormattedExcinfo(object):
reprcrash = None reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)] repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None: if e.__cause__ is not None and self.chain:
e = e.__cause__ e = e.__cause__
excinfo = ( excinfo = (
ExceptionInfo((type(e), e, e.__traceback__)) ExceptionInfo((type(e), e, e.__traceback__))
@ -743,7 +769,11 @@ class FormattedExcinfo(object):
else None else None
) )
descr = "The above exception was the direct cause of the following exception:" 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__ e = e.__context__
excinfo = ( excinfo = (
ExceptionInfo((type(e), e, e.__traceback__)) ExceptionInfo((type(e), e, e.__traceback__))
@ -979,3 +1009,36 @@ else:
return "maximum recursion depth exceeded" in str(excinfo.value) return "maximum recursion depth exceeded" in str(excinfo.value)
except UnicodeError: except UnicodeError:
return False 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)
)

View File

@ -17,8 +17,9 @@ import atomicwrites
import py import py
from _pytest.assertion import util from _pytest.assertion import util
from _pytest.compat import PurePath, spec_from_file_location from _pytest.pathlib import PurePath
from _pytest.paths import fnmatch_ex from _pytest.compat import spec_from_file_location
from _pytest.pathlib import fnmatch_ex
# pytest caches rewritten pycs in __pycache__. # pytest caches rewritten pycs in __pycache__.
if hasattr(imp, "get_tag"): if hasattr(imp, "get_tag"):

View File

@ -13,10 +13,9 @@ import attr
import pytest import pytest
import json import json
import shutil
from . import paths from .compat import _PY2 as PY2
from .compat import _PY2 as PY2, Path from .pathlib import Path, resolve_from_str, rmtree
README_CONTENT = u"""\ README_CONTENT = u"""\
# pytest cache directory # # pytest cache directory #
@ -39,13 +38,13 @@ class Cache(object):
def for_config(cls, config): def for_config(cls, config):
cachedir = cls.cache_dir_from_config(config) cachedir = cls.cache_dir_from_config(config)
if config.getoption("cacheclear") and cachedir.exists(): if config.getoption("cacheclear") and cachedir.exists():
shutil.rmtree(str(cachedir)) rmtree(cachedir, force=True)
cachedir.mkdir() cachedir.mkdir()
return cls(cachedir, config) return cls(cachedir, config)
@staticmethod @staticmethod
def cache_dir_from_config(config): 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): def warn(self, fmt, **args):
from _pytest.warnings import _issue_config_warning from _pytest.warnings import _issue_config_warning

View File

@ -23,8 +23,6 @@ except ImportError: # pragma: no cover
# Only available in Python 3.4+ or as a backport # Only available in Python 3.4+ or as a backport
enum = None enum = None
__all__ = ["Path", "PurePath"]
_PY3 = sys.version_info > (3, 0) _PY3 = sys.version_info > (3, 0)
_PY2 = not _PY3 _PY2 = not _PY3
@ -41,11 +39,6 @@ PY35 = sys.version_info[:2] >= (3, 5)
PY36 = sys.version_info[:2] >= (3, 6) PY36 = sys.version_info[:2] >= (3, 6)
MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError" MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError"
if PY36:
from pathlib import Path, PurePath
else:
from pathlib2 import Path, PurePath
if _PY3: if _PY3:
from collections.abc import MutableMapping as MappingMixin from collections.abc import MutableMapping as MappingMixin

View File

@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function
import argparse import argparse
import inspect import inspect
import shlex import shlex
import traceback
import types import types
import warnings import warnings
import copy import copy
@ -19,6 +18,7 @@ import _pytest._code
import _pytest.hookspec # the extension point definitions import _pytest.hookspec # the extension point definitions
import _pytest.assertion import _pytest.assertion
from pluggy import PluginManager, HookimplMarker, HookspecMarker from pluggy import PluginManager, HookimplMarker, HookspecMarker
from _pytest._code import ExceptionInfo, filter_traceback
from _pytest.compat import safe_str from _pytest.compat import safe_str
from .exceptions import UsageError, PrintHelp from .exceptions import UsageError, PrintHelp
from .findpaths import determine_setup, exists from .findpaths import determine_setup, exists
@ -26,9 +26,6 @@ from .findpaths import determine_setup, exists
hookimpl = HookimplMarker("pytest") hookimpl = HookimplMarker("pytest")
hookspec = HookspecMarker("pytest") hookspec = HookspecMarker("pytest")
# pytest startup
#
class ConftestImportFailure(Exception): class ConftestImportFailure(Exception):
def __init__(self, path, excinfo): def __init__(self, path, excinfo):
@ -36,12 +33,6 @@ class ConftestImportFailure(Exception):
self.path = path self.path = path
self.excinfo = excinfo 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): def main(args=None, plugins=None):
""" return exit code, after performing an in-process test run. """ return exit code, after performing an in-process test run.
@ -57,10 +48,20 @@ def main(args=None, plugins=None):
try: try:
config = _prepareconfig(args, plugins) config = _prepareconfig(args, plugins)
except ConftestImportFailure as e: except ConftestImportFailure as e:
exc_info = ExceptionInfo(e.excinfo)
tw = py.io.TerminalWriter(sys.stderr) 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(line.rstrip(), red=True)
tw.line("ERROR: could not load %s\n" % (e.path,), red=True)
return 4 return 4
else: else:
try: try:
@ -378,25 +379,27 @@ class PytestPluginManager(PluginManager):
def _getconftestmodules(self, path): def _getconftestmodules(self, path):
if self._noconftest: if self._noconftest:
return [] return []
try:
return self._path2confmods[path]
except KeyError:
if path.isfile():
clist = self._getconftestmodules(path.dirpath())
else:
# 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():
if self._confcutdir and self._confcutdir.relto(parent):
continue
conftestpath = parent.join("conftest.py")
if conftestpath.isfile():
mod = self._importconftest(conftestpath)
clist.append(mod)
self._path2confmods[path] = clist if path.isfile():
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 directory.parts():
if self._confcutdir and self._confcutdir.relto(parent):
continue
conftestpath = parent.join("conftest.py")
if conftestpath.isfile():
mod = self._importconftest(conftestpath)
clist.append(mod)
self._path2confmods[directory] = clist
return clist return clist
def _rget_with_confmod(self, name, path): def _rget_with_confmod(self, name, path):

View File

@ -103,21 +103,18 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None):
if inifile: if inifile:
iniconfig = py.iniconfig.IniConfig(inifile) iniconfig = py.iniconfig.IniConfig(inifile)
is_cfg_file = str(inifile).endswith(".cfg") 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"] sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"]
for section in sections: for section in sections:
try: try:
inicfg = iniconfig[section] inicfg = iniconfig[section]
if is_cfg_file and section == "pytest" and config is not None: if is_cfg_file and section == "pytest" and config is not None:
from _pytest.deprecated import CFG_PYTEST_SECTION from _pytest.deprecated import CFG_PYTEST_SECTION
from _pytest.warning_types import RemovedInPytest4Warning
from _pytest.warnings import _issue_config_warning from _pytest.warnings import _issue_config_warning
# TODO: [pytest] section in *.cfg files is deprecated. Need refactoring once
# the deprecation expires.
_issue_config_warning( _issue_config_warning(
RemovedInPytest4Warning( CFG_PYTEST_SECTION.format(filename=str(inifile)), config
CFG_PYTEST_SECTION.format(filename=str(inifile))
),
config,
) )
break break
except KeyError: except KeyError:

View File

@ -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 Keeping it in a central location makes it easy to track what is deprecated and should
be removed when the time comes. 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 __future__ import absolute_import, division, print_function
from _pytest.warning_types import RemovedInPytest4Warning
from _pytest.warning_types import UnformattedWarning, RemovedInPytest4Warning
MAIN_STR_ARGS = RemovedInPytest4Warning( MAIN_STR_ARGS = RemovedInPytest4Warning(
"passing a string to pytest.main() is deprecated, " "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" "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 ' '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated '
"and scheduled to be removed in pytest 4.0. " "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, ' 'Fixture "{name}" called directly. Fixtures are not meant to be called directly, '
"are created automatically when test functions request them as parameters. " "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 = ( CFG_PYTEST_SECTION = UnformattedWarning(
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead." 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" "--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." "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 = RemovedInPytest4Warning(
"pytest_namespace is deprecated and will be removed soon" "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"
)

View File

@ -32,7 +32,7 @@ from _pytest.compat import (
get_real_method, get_real_method,
_PytestWrapper, _PytestWrapper,
) )
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning from _pytest.deprecated import FIXTURE_FUNCTION_CALL
from _pytest.outcomes import fail, TEST_OUTCOME from _pytest.outcomes import fail, TEST_OUTCOME
FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}' 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. or ``session`` indicating the caching lifecycle of the resource.
:arg extrakey: added to internal caching key of (funcargname, scope). :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"): if not hasattr(self.config, "_setupcache"):
self.config._setupcache = {} # XXX weakref? self.config._setupcache = {} # XXX weakref?
cachekey = (self.fixturename, self._getscopeitem(scope), extrakey) cachekey = (self.fixturename, self._getscopeitem(scope), extrakey)
@ -514,7 +517,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
""" Deprecated, use getfixturevalue. """ """ Deprecated, use getfixturevalue. """
from _pytest import deprecated from _pytest import deprecated
warnings.warn(deprecated.GETFUNCARGVALUE, DeprecationWarning, stacklevel=2) warnings.warn(deprecated.GETFUNCARGVALUE, stacklevel=2)
return self.getfixturevalue(argname) return self.getfixturevalue(argname)
def _get_active_fixturedef(self, argname): def _get_active_fixturedef(self, argname):
@ -576,7 +579,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
nodeid=funcitem.nodeid, nodeid=funcitem.nodeid,
typename=type(funcitem).__name__, typename=type(funcitem).__name__,
) )
fail(msg) fail(msg, pytrace=False)
if has_params: if has_params:
frame = inspect.stack()[3] frame = inspect.stack()[3]
frameinfo = inspect.getframeinfo(frame[0]) frameinfo = inspect.getframeinfo(frame[0])
@ -597,7 +600,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
source_lineno, source_lineno,
) )
) )
fail(msg) fail(msg, pytrace=False)
else: else:
# indices might not be set if old-style metafunc.addcall() was used # indices might not be set if old-style metafunc.addcall() was used
param_index = funcitem.callspec.indices.get(argname, 0) param_index = funcitem.callspec.indices.get(argname, 0)
@ -715,10 +718,11 @@ def scope2index(scope, descr, where=None):
try: try:
return scopes.index(scope) return scopes.index(scope)
except ValueError: except ValueError:
raise ValueError( fail(
"{} {}has an unsupported scope value '{}'".format( "{} {}got an unexpected scope value '{}'".format(
descr, "from {} ".format(where) if where else "", scope descr, "from {} ".format(where) if where else "", scope
) ),
pytrace=False,
) )
@ -851,7 +855,9 @@ class FixtureDef(object):
self.argname = argname self.argname = argname
self.scope = scope self.scope = scope
self.scopenum = scope2index( 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.params = params
self.argnames = getfuncargnames(func, is_method=unittest) self.argnames = getfuncargnames(func, is_method=unittest)
@ -913,7 +919,7 @@ class FixtureDef(object):
return hook.pytest_fixture_setup(fixturedef=self, request=request) return hook.pytest_fixture_setup(fixturedef=self, request=request)
def __repr__(self): def __repr__(self):
return "<FixtureDef name=%r scope=%r baseid=%r >" % ( return "<FixtureDef name=%r scope=%r baseid=%r>" % (
self.argname, self.argname,
self.scope, self.scope,
self.baseid, self.baseid,
@ -973,8 +979,9 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker):
used as an argument in a test function. used as an argument in a test function.
""" """
is_yield_function = is_generator(function) is_yield_function = is_generator(function)
msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__) warning = FIXTURE_FUNCTION_CALL.format(
warning = RemovedInPytest4Warning(msg) name=fixture_marker.name or function.__name__
)
if is_yield_function: if is_yield_function:
@ -1168,7 +1175,7 @@ class FixtureManager(object):
def pytest_plugin_registered(self, plugin): def pytest_plugin_registered(self, plugin):
nodeid = None nodeid = None
try: try:
p = py.path.local(plugin.__file__) p = py.path.local(plugin.__file__).realpath()
except AttributeError: except AttributeError:
pass pass
else: else:
@ -1301,9 +1308,7 @@ class FixtureManager(object):
filename, lineno = getfslineno(obj) filename, lineno = getfslineno(obj)
warnings.warn_explicit( warnings.warn_explicit(
RemovedInPytest4Warning( deprecated.FUNCARG_PREFIX.format(name=name),
deprecated.FUNCARG_PREFIX.format(name=name)
),
category=None, category=None,
filename=str(filename), filename=str(filename),
lineno=lineno + 1, lineno=lineno + 1,

View File

@ -2,7 +2,7 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import logging import logging
from contextlib import closing, contextmanager from contextlib import contextmanager
import re import re
import six import six
@ -415,7 +415,6 @@ class LoggingPlugin(object):
else: else:
self.log_file_handler = None self.log_file_handler = None
# initialized during pytest_runtestloop
self.log_cli_handler = None self.log_cli_handler = None
def _log_cli_enabled(self): def _log_cli_enabled(self):
@ -426,6 +425,22 @@ class LoggingPlugin(object):
"--log-cli-level" "--log-cli-level"
) is not None or self._config.getini("log_cli") ) 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 @contextmanager
def _runtest_for(self, item, when): def _runtest_for(self, item, when):
"""Implements the internals of pytest_runtest_xxx() hook.""" """Implements the internals of pytest_runtest_xxx() hook."""
@ -485,22 +500,15 @@ class LoggingPlugin(object):
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtestloop(self, session): def pytest_runtestloop(self, session):
"""Runs all collected test items.""" """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: 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( yield # run all the tests
self.log_file_handler, level=self.log_file_level
):
yield # run all the tests
else: else:
yield # run all the tests yield # run all the tests
def _setup_cli_logging(self): def _setup_cli_logging(self):
"""Sets up the handler and logger for the Live Logs feature, if enabled. """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.
"""
terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter")
if self._log_cli_enabled() and terminal_reporter is not None: if self._log_cli_enabled() and terminal_reporter is not None:
capture_manager = self._config.pluginmanager.get_plugin("capturemanager") capture_manager = self._config.pluginmanager.get_plugin("capturemanager")
@ -530,11 +538,14 @@ class LoggingPlugin(object):
self._config, "log_cli_level", "log_level" self._config, "log_cli_level", "log_level"
) )
self.log_cli_handler = log_cli_handler 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 log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
) )
else: 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): class _LiveLoggingStreamHandler(logging.StreamHandler):

View File

@ -156,7 +156,10 @@ def pytest_addoption(parser):
dest="basetemp", dest="basetemp",
default=None, default=None,
metavar="dir", 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 session.exitstatus = EXIT_TESTSFAILED
except KeyboardInterrupt: except KeyboardInterrupt:
excinfo = _pytest._code.ExceptionInfo() 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)) 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) config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
session.exitstatus = EXIT_INTERRUPTED session.exitstatus = exitstatus
except: # noqa except: # noqa
excinfo = _pytest._code.ExceptionInfo() excinfo = _pytest._code.ExceptionInfo()
config.notify_exception(excinfo, config.option) config.notify_exception(excinfo, config.option)
@ -487,7 +493,7 @@ class Session(nodes.FSCollector):
from _pytest.python import Package from _pytest.python import Package
names = self._parsearg(arg) names = self._parsearg(arg)
argpath = names.pop(0) argpath = names.pop(0).realpath()
paths = [] paths = []
root = self root = self

View File

@ -24,11 +24,6 @@ __all__ = [
] ]
class MarkerError(Exception):
"""Error in use of a pytest marker/attribute."""
def param(*values, **kw): def param(*values, **kw):
"""Specify a parameter in `pytest.mark.parametrize`_ calls or """Specify a parameter in `pytest.mark.parametrize`_ calls or
:ref:`parametrized fixtures <fixture-parametrize-marks>`. :ref:`parametrized fixtures <fixture-parametrize-marks>`.
@ -163,9 +158,9 @@ def pytest_configure(config):
empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) 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( 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) " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset)
) )

View File

@ -6,6 +6,7 @@ from operator import attrgetter
import attr import attr
from _pytest.outcomes import fail
from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE
from ..compat import NOTSET, getfslineno, MappingMixin from ..compat import NOTSET, getfslineno, MappingMixin
from six.moves import map from six.moves import map
@ -32,11 +33,19 @@ def istestfunc(func):
def get_empty_parameterset_mark(config, argnames, func): def get_empty_parameterset_mark(config, argnames, func):
from ..nodes import Collector
requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
if requested_mark in ("", None, "skip"): if requested_mark in ("", None, "skip"):
mark = MARK_GEN.skip mark = MARK_GEN.skip
elif requested_mark == "xfail": elif requested_mark == "xfail":
mark = MARK_GEN.xfail(run=False) 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: else:
raise LookupError(requested_mark) raise LookupError(requested_mark)
fs, lineno = getfslineno(func) fs, lineno = getfslineno(func)
@ -307,7 +316,7 @@ def _marked(func, mark):
return any(mark == info.combined for info in func_mark) return any(mark == info.combined for info in func_mark)
@attr.s @attr.s(repr=False)
class MarkInfo(object): class MarkInfo(object):
""" Marking object created by :class:`MarkDecorator` instances. """ """ Marking object created by :class:`MarkDecorator` instances. """
@ -385,7 +394,7 @@ class MarkGenerator(object):
x = marker.split("(", 1)[0] x = marker.split("(", 1)[0]
values.add(x) values.add(x)
if name not in self._markers: 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() MARK_GEN = MarkGenerator()

View File

@ -9,6 +9,7 @@ import attr
import _pytest import _pytest
import _pytest._code import _pytest._code
from _pytest.compat import getfslineno from _pytest.compat import getfslineno
from _pytest.outcomes import fail
from _pytest.mark.structures import NodeKeywords, MarkInfo from _pytest.mark.structures import NodeKeywords, MarkInfo
@ -61,11 +62,11 @@ class _CompatProperty(object):
if obj is None: if obj is None:
return self return self
# TODO: reenable in the features branch from _pytest.deprecated import COMPAT_PROPERTY
# warnings.warn(
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format( warnings.warn(
# name=self.name, owner=type(owner).__name__), COMPAT_PROPERTY.format(name=self.name, owner=owner.__name__), stacklevel=2
# PendingDeprecationWarning, stacklevel=2) )
return getattr(__import__("pytest"), self.name) return getattr(__import__("pytest"), self.name)
@ -126,11 +127,10 @@ class Node(object):
if isinstance(maybe_compatprop, _CompatProperty): if isinstance(maybe_compatprop, _CompatProperty):
return getattr(__import__("pytest"), name) return getattr(__import__("pytest"), name)
else: else:
from _pytest.deprecated import CUSTOM_CLASS
cls = getattr(self, name) cls = getattr(self, name)
# TODO: reenable in the features branch self.warn(CUSTOM_CLASS.format(name=name, type_name=type(self).__name__))
# warnings.warn("use of node.%s is deprecated, "
# "use pytest_pycollect_makeitem(...) to create custom "
# "collection nodes" % name, category=DeprecationWarning)
return cls return cls
def __repr__(self): def __repr__(self):
@ -347,6 +347,9 @@ class Node(object):
pass pass
def _repr_failure_py(self, excinfo, style=None): 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 fm = self.session._fixturemanager
if excinfo.errisinstance(fm.FixtureLookupError): if excinfo.errisinstance(fm.FixtureLookupError):
return excinfo.value.formatrepr() return excinfo.value.formatrepr()

View File

@ -49,18 +49,24 @@ class Failed(OutcomeException):
class Exit(KeyboardInterrupt): class Exit(KeyboardInterrupt):
""" raised for immediate program exits (no tracebacks/summaries)""" """ 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.msg = msg
self.returncode = returncode
KeyboardInterrupt.__init__(self, msg) KeyboardInterrupt.__init__(self, msg)
# exposed helper methods # exposed helper methods
def exit(msg): def exit(msg, returncode=None):
""" exit testing process as if KeyboardInterrupt was triggered. """ """
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 __tracebackhide__ = True
raise Exit(msg) raise Exit(msg, returncode)
exit.Exception = Exit exit.Exception = Exit

283
src/_pytest/pathlib.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -17,13 +17,14 @@ from weakref import WeakKeyDictionary
from _pytest.capture import MultiCapture, SysCapture from _pytest.capture import MultiCapture, SysCapture
from _pytest._code import Source from _pytest._code import Source
import py
import pytest
from _pytest.main import Session, EXIT_INTERRUPTED, EXIT_OK from _pytest.main import Session, EXIT_INTERRUPTED, EXIT_OK
from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.assertion.rewrite import AssertionRewritingHook
from _pytest.compat import Path from _pytest.pathlib import Path
from _pytest.compat import safe_str from _pytest.compat import safe_str
import py
import pytest
IGNORE_PAM = [ # filenames added when obtaining details about the current user IGNORE_PAM = [ # filenames added when obtaining details about the current user
u"/var/lib/sss/mc/passwd" u"/var/lib/sss/mc/passwd"
] ]
@ -61,6 +62,11 @@ def pytest_configure(config):
config.pluginmanager.register(checker) config.pluginmanager.register(checker)
def raise_on_kwargs(kwargs):
if kwargs:
raise TypeError("Unexpected arguments: {}".format(", ".join(sorted(kwargs))))
class LsofFdLeakChecker(object): class LsofFdLeakChecker(object):
def get_open_files(self): def get_open_files(self):
out = self._exec_lsof() out = self._exec_lsof()
@ -482,11 +488,16 @@ class Testdir(object):
""" """
class TimeoutExpired(Exception):
pass
def __init__(self, request, tmpdir_factory): def __init__(self, request, tmpdir_factory):
self.request = request self.request = request
self._mod_collections = WeakKeyDictionary() self._mod_collections = WeakKeyDictionary()
name = request.function.__name__ name = request.function.__name__
self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) 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.plugins = []
self._cwd_snapshot = CwdSnapshot() self._cwd_snapshot = CwdSnapshot()
self._sys_path_snapshot = SysPathsSnapshot() self._sys_path_snapshot = SysPathsSnapshot()
@ -513,6 +524,7 @@ class Testdir(object):
self._sys_modules_snapshot.restore() self._sys_modules_snapshot.restore()
self._sys_path_snapshot.restore() self._sys_path_snapshot.restore()
self._cwd_snapshot.restore() self._cwd_snapshot.restore()
os.environ.pop("PYTEST_DEBUG_TEMPROOT", None)
def __take_sys_modules_snapshot(self): def __take_sys_modules_snapshot(self):
# some zope modules used by twisted-related tests keep internal state # some zope modules used by twisted-related tests keep internal state
@ -1039,14 +1051,23 @@ class Testdir(object):
return popen return popen
def run(self, *cmdargs): def run(self, *cmdargs, **kwargs):
"""Run a command with arguments. """Run a command with arguments.
Run a process using subprocess.Popen saving the stdout and stderr. 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`. Returns a :py:class:`RunResult`.
""" """
__tracebackhide__ = True
timeout = kwargs.pop("timeout", None)
raise_on_kwargs(kwargs)
cmdargs = [ cmdargs = [
str(arg) if isinstance(arg, py.path.local) else arg for arg in 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( popen = self.popen(
cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32")
) )
ret = popen.wait()
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: finally:
f1.close() f1.close()
f2.close() f2.close()
@ -1108,9 +1162,15 @@ class Testdir(object):
with "runpytest-" so they do not conflict with the normal numbered with "runpytest-" so they do not conflict with the normal numbered
pytest location for temporary files and directories. 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`. Returns a :py:class:`RunResult`.
""" """
__tracebackhide__ = True
p = py.path.local.make_numbered_dir( p = py.path.local.make_numbered_dir(
prefix="runpytest-", keep=None, rootdir=self.tmpdir prefix="runpytest-", keep=None, rootdir=self.tmpdir
) )
@ -1119,7 +1179,7 @@ class Testdir(object):
if plugins: if plugins:
args = ("-p", plugins[0]) + args args = ("-p", plugins[0]) + args
args = self._getpytestargs() + 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): def spawn_pytest(self, string, expect_timeout=10.0):
"""Run pytest using pexpect. """Run pytest using pexpect.
@ -1267,6 +1327,7 @@ class LineMatcher(object):
matches and non-matches are also printed on stdout. matches and non-matches are also printed on stdout.
""" """
__tracebackhide__ = True
self._match_lines(lines2, fnmatch, "fnmatch") self._match_lines(lines2, fnmatch, "fnmatch")
def re_match_lines(self, lines2): def re_match_lines(self, lines2):
@ -1278,6 +1339,7 @@ class LineMatcher(object):
The matches and non-matches are also printed on stdout. 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") self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match")
def _match_lines(self, lines2, match_func, match_nickname): def _match_lines(self, lines2, match_func, match_nickname):

View File

@ -13,11 +13,10 @@ from textwrap import dedent
import py import py
import six import six
from _pytest.main import FSHookProxy from _pytest.main import FSHookProxy
from _pytest.mark import MarkerError
from _pytest.config import hookimpl from _pytest.config import hookimpl
import _pytest import _pytest
import pluggy from _pytest._code import filter_traceback
from _pytest import fixtures from _pytest import fixtures
from _pytest import nodes from _pytest import nodes
from _pytest import deprecated from _pytest import deprecated
@ -47,37 +46,6 @@ from _pytest.mark.structures import (
) )
from _pytest.warning_types import RemovedInPytest4Warning, PytestWarning 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 pyobj_property(name):
def get(self): def get(self):
@ -159,8 +127,8 @@ def pytest_generate_tests(metafunc):
alt_spellings = ["parameterize", "parametrise", "parameterise"] alt_spellings = ["parameterize", "parametrise", "parameterise"]
for attr in alt_spellings: for attr in alt_spellings:
if hasattr(metafunc.function, attr): if hasattr(metafunc.function, attr):
msg = "{0} has '{1}', spelling should be 'parametrize'" msg = "{0} has '{1}' mark, spelling should be 'parametrize'"
raise MarkerError(msg.format(metafunc.function.__name__, attr)) fail(msg.format(metafunc.function.__name__, attr), pytrace=False)
for marker in metafunc.definition.iter_markers(name="parametrize"): for marker in metafunc.definition.iter_markers(name="parametrize"):
metafunc.parametrize(*marker.args, **marker.kwargs) metafunc.parametrize(*marker.args, **marker.kwargs)
@ -760,12 +728,6 @@ class FunctionMixin(PyobjMixin):
for entry in excinfo.traceback[1:-1]: for entry in excinfo.traceback[1:-1]:
entry.set_repr_style("short") 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): def repr_failure(self, excinfo, outerr=None):
assert outerr is None, "XXX outerr usage is deprecated" assert outerr is None, "XXX outerr usage is deprecated"
style = self.config.option.tbstyle style = self.config.option.tbstyle
@ -799,7 +761,10 @@ class Generator(FunctionMixin, PyCollector):
"%r generated tests with non-unique name %r" % (self, name) "%r generated tests with non-unique name %r" % (self, name)
) )
seen[name] = True 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) self.warn(deprecated.YIELD_TESTS)
return values return values
@ -984,7 +949,9 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition) 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 # 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 # more than once) then we accumulate those calls generating the cartesian product
@ -1023,15 +990,16 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
idfn = ids idfn = ids
ids = None ids = None
if ids: if ids:
func_name = self.function.__name__
if len(ids) != len(parameters): if len(ids) != len(parameters):
raise ValueError( msg = "In {}: {} parameter sets specified, with different number of ids: {}"
"%d tests specified with %d ids" % (len(parameters), len(ids)) fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False)
)
for id_value in ids: for id_value in ids:
if id_value is not None and not isinstance(id_value, six.string_types): 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)" msg = "In {}: ids must be list of strings, found: {} (type: {!r})"
raise ValueError( fail(
msg % (saferepr(id_value), type(id_value).__name__) msg.format(func_name, saferepr(id_value), type(id_value)),
pytrace=False,
) )
ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item)
return ids return ids
@ -1056,9 +1024,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
valtypes = dict.fromkeys(argnames, "funcargs") valtypes = dict.fromkeys(argnames, "funcargs")
for arg in indirect: for arg in indirect:
if arg not in argnames: if arg not in argnames:
raise ValueError( fail(
"indirect given to %r: fixture %r doesn't exist" "In {}: indirect fixture '{}' doesn't exist".format(
% (self.function, arg) self.function.__name__, arg
),
pytrace=False,
) )
valtypes[arg] = "params" valtypes[arg] = "params"
return valtypes return valtypes
@ -1072,19 +1042,25 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
:raise ValueError: if validation fails. :raise ValueError: if validation fails.
""" """
default_arg_names = set(get_default_arg_names(self.function)) default_arg_names = set(get_default_arg_names(self.function))
func_name = self.function.__name__
for arg in argnames: for arg in argnames:
if arg not in self.fixturenames: if arg not in self.fixturenames:
if arg in default_arg_names: if arg in default_arg_names:
raise ValueError( fail(
"%r already takes an argument %r with a default value" "In {}: function already takes an argument '{}' with a default value".format(
% (self.function, arg) func_name, arg
),
pytrace=False,
) )
else: else:
if isinstance(indirect, (tuple, list)): if isinstance(indirect, (tuple, list)):
name = "fixture" if arg in indirect else "argument" name = "fixture" if arg in indirect else "argument"
else: else:
name = "fixture" if indirect else "argument" 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): 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. """ Add a new call to the underlying test function during the collection phase of a test run.

View File

@ -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 in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
types above. types above.
""" """
if not func: __tracebackhide__ = True
return _DeprecatedCallContext() if func is not None:
else: args = (func,) + args
__tracebackhide__ = True return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs)
with _DeprecatedCallContext():
return func(*args, **kwargs)
class _DeprecatedCallContext(object):
"""Implements the logic to capture deprecation warnings as a context manager."""
def __enter__(self):
self._captured_categories = []
self._old_warn = warnings.warn
self._old_warn_explicit = warnings.warn_explicit
warnings.warn_explicit = self._warn_explicit
warnings.warn = self._warn
def _warn_explicit(self, message, category, *args, **kwargs):
self._captured_categories.append(category)
def _warn(self, message, category=None, *args, **kwargs):
if isinstance(message, Warning):
self._captured_categories.append(message.__class__)
else:
self._captured_categories.append(category)
def __exit__(self, exc_type, exc_val, exc_tb):
warnings.warn_explicit = self._old_warn_explicit
warnings.warn = self._old_warn
if exc_type is None:
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
if not any(
issubclass(c, deprecation_categories) for c in self._captured_categories
):
__tracebackhide__ = True
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
raise AssertionError(msg)
def warns(expected_warning, *args, **kwargs): 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... Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
""" """
__tracebackhide__ = True
match_expr = None match_expr = None
if not args: if not args:
if "match" in kwargs: if "match" in kwargs:
@ -183,12 +149,25 @@ class WarningsRecorder(warnings.catch_warnings):
raise RuntimeError("Cannot enter %r twice" % self) raise RuntimeError("Cannot enter %r twice" % self)
self._list = super(WarningsRecorder, self).__enter__() self._list = super(WarningsRecorder, self).__enter__()
warnings.simplefilter("always") 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 return self
def __exit__(self, *exc_info): def __exit__(self, *exc_info):
if not self._entered: if not self._entered:
__tracebackhide__ = True __tracebackhide__ = True
raise RuntimeError("Cannot exit %r without entering first" % self) 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) super(WarningsRecorder, self).__exit__(*exc_info)

View File

@ -31,10 +31,9 @@ def pytest_configure(config):
config.pluginmanager.register(config._resultlog) config.pluginmanager.register(config._resultlog)
from _pytest.deprecated import RESULT_LOG from _pytest.deprecated import RESULT_LOG
from _pytest.warning_types import RemovedInPytest4Warning
from _pytest.warnings import _issue_config_warning from _pytest.warnings import _issue_config_warning
_issue_config_warning(RemovedInPytest4Warning(RESULT_LOG), config) _issue_config_warning(RESULT_LOG, config)
def pytest_unconfigure(config): def pytest_unconfigure(config):

View File

@ -676,7 +676,9 @@ class TerminalReporter(object):
if fspath: if fspath:
res = mkrel(nodeid).replace("::()", "") # parens-normalization 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) res += " <- " + self.startdir.bestrelpath(fspath)
else: else:
res = "[location]" res = "[location]"

View File

@ -1,22 +1,86 @@
""" support for providing temporary directories to test functions. """ """ support for providing temporary directories to test functions. """
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import os
import re import re
import pytest import pytest
import py import py
from _pytest.monkeypatch import MonkeyPatch 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. """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): _tmppath_factory = attr.ib()
self.config = config
self.trace = config.trace.get("tmpdir")
def ensuretemp(self, string, dir=1): def ensuretemp(self, string, dir=1):
""" (deprecated) return temporary directory path with """ (deprecated) return temporary directory path with
@ -26,6 +90,9 @@ class TempdirFactory(object):
and is guaranteed to be empty. and is guaranteed to be empty.
""" """
# py.log._apiwarn(">1.1", "use tmpdir function argument") # 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) return self.getbasetemp().ensure(string, dir=dir)
def mktemp(self, basename, numbered=True): def mktemp(self, basename, numbered=True):
@ -33,46 +100,11 @@ class TempdirFactory(object):
If ``numbered``, ensure the directory is unique by adding a number If ``numbered``, ensure the directory is unique by adding a number
prefix greater than any existing one. prefix greater than any existing one.
""" """
basetemp = self.getbasetemp() return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())
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
def getbasetemp(self): def getbasetemp(self):
""" return base temporary directory. """ """backward compat wrapper for ``_tmppath_factory.getbasetemp``"""
try: return py.path.local(self._tmppath_factory.getbasetemp().resolve())
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")
def get_user(): def get_user():
@ -87,10 +119,6 @@ def get_user():
return None return None
# backward compatibility
TempdirHandler = TempdirFactory
def pytest_configure(config): def pytest_configure(config):
"""Create a TempdirFactory and attach it to the config object. """Create a TempdirFactory and attach it to the config object.
@ -99,19 +127,36 @@ def pytest_configure(config):
to the tmpdir_factory session fixture. to the tmpdir_factory session fixture.
""" """
mp = MonkeyPatch() mp = MonkeyPatch()
t = TempdirFactory(config) tmppath_handler = TempPathFactory.from_config(config)
config._cleanup.extend([mp.undo, t.finish]) 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(config, "_tmpdirhandler", t, raising=False)
mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False) mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def tmpdir_factory(request): 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 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 @pytest.fixture
def tmpdir(request, tmpdir_factory): def tmpdir(request, tmpdir_factory):
"""Return a temporary directory path object """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 .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
""" """
name = request.node.name return _mk_tmp(request, tmpdir_factory)
name = re.sub(r"[\W]", "_", name)
MAXVAL = 30
if len(name) > MAXVAL: @pytest.fixture
name = name[:MAXVAL] def tmp_path(request, tmp_path_factory):
x = tmpdir_factory.mktemp(name, numbered=True) """Return a temporary directory path object
return x 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)

View File

@ -1,3 +1,6 @@
import attr
class PytestWarning(UserWarning): class PytestWarning(UserWarning):
""" """
Bases: :class:`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") PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example")

View File

@ -67,27 +67,27 @@ def catch_warnings_for_item(config, ihook, when, item):
Each warning captured triggers the ``pytest_warning_captured`` hook. Each warning captured triggers the ``pytest_warning_captured`` hook.
""" """
args = config.getoption("pythonwarnings") or [] cmdline_filters = config.getoption("pythonwarnings") or []
inifilters = config.getini("filterwarnings") inifilters = config.getini("filterwarnings")
with warnings.catch_warnings(record=True) as log: with warnings.catch_warnings(record=True) as log:
filters_configured = args or inifilters or sys.warnoptions
for arg in args: if not sys.warnoptions:
warnings._setoption(arg) # 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: for arg in inifilters:
_setoption(warnings, arg) _setoption(warnings, arg)
for arg in cmdline_filters:
warnings._setoption(arg)
if item is not None: if item is not None:
for mark in item.iter_markers(name="filterwarnings"): for mark in item.iter_markers(name="filterwarnings"):
for arg in mark.args: for arg in mark.args:
_setoption(warnings, arg) _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 yield

View File

@ -140,9 +140,16 @@ class TestGeneralUsage(object):
assert result.ret assert result.ret
result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)]) 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.makepyfile("")
testdir.makeconftest("import qwerty") testdir.makeconftest(
"""
def foo():
import qwerty
foo()
"""
)
result = testdir.runpytest("--help") result = testdir.runpytest("--help")
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" """
@ -151,10 +158,23 @@ class TestGeneralUsage(object):
""" """
) )
result = testdir.runpytest() result = testdir.runpytest()
dirname = request.node.name + "0"
exc_name = (
"ModuleNotFoundError" if sys.version_info >= (3, 6) else "ImportError"
)
result.stderr.fnmatch_lines( 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): def test_early_skip(self, testdir):
@ -723,16 +743,26 @@ class TestInvocationVariants(object):
monkeypatch.syspath_prepend(p) monkeypatch.syspath_prepend(p)
# module picked up in symlink-ed directory: # 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") result = testdir.runpytest("--pyargs", "-v", "foo.bar")
testdir.chdir() testdir.chdir()
assert result.ret == 0 assert result.ret == 0
result.stdout.fnmatch_lines( 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*", "lib/foo/bar/test_bar.py::test_bar PASSED*",
"*2 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*",
"*lib/foo/bar/test_bar.py::test_other PASSED*",
"*2 passed*",
]
)
def test_cmdline_python_package_not_exists(self, testdir): def test_cmdline_python_package_not_exists(self, testdir):
result = testdir.runpytest("--pyargs", "tpkgwhatv") result = testdir.runpytest("--pyargs", "tpkgwhatv")

View File

@ -1184,20 +1184,28 @@ raise ValueError()
assert tw.lines[47] == ":15: AttributeError" assert tw.lines[47] == ":15: AttributeError"
@pytest.mark.skipif("sys.version_info[0] < 3") @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( mod = importasmod(
""" """
def f(): def f():
try: try:
g() g()
except Exception: except Exception:
raise AttributeError() from None raise AttributeError(){raise_suffix}
def g(): def g():
raise ValueError() raise ValueError()
""" """.format(
raise_suffix=raise_suffix
)
) )
excinfo = pytest.raises(AttributeError, mod.f) excinfo = pytest.raises(AttributeError, mod.f)
r = excinfo.getrepr(style="long") r = excinfo.getrepr(style="long", chain=mode != "explicit_suppress")
tw = TWMock() tw = TWMock()
r.toterminal(tw) r.toterminal(tw)
for line in tw.lines: for line in tw.lines:
@ -1207,7 +1215,9 @@ raise ValueError()
assert tw.lines[2] == " try:" assert tw.lines[2] == " try:"
assert tw.lines[3] == " g()" assert tw.lines[3] == " g()"
assert tw.lines[4] == " except Exception:" 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[6] == "E AttributeError"
assert tw.lines[7] == "" assert tw.lines[7] == ""
line = tw.get_write_msg(8) line = tw.get_write_msg(8)

View File

@ -30,6 +30,74 @@ def test_yield_tests_deprecation(testdir):
assert result.stdout.str().count("yield tests are deprecated") == 2 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") @pytest.mark.filterwarnings("default")
def test_funcarg_prefix_deprecation(testdir): def test_funcarg_prefix_deprecation(testdir):
testdir.makepyfile( testdir.makepyfile(

View File

@ -908,3 +908,61 @@ def test_live_logging_suspends_capture(has_capture_manager, request):
else: else:
assert MockCaptureManager.calls == [] assert MockCaptureManager.calls == []
assert out_file.getvalue() == "\nsome message\n" 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

View File

@ -240,6 +240,9 @@ class TestClass(object):
assert result.ret == EXIT_NOTESTSCOLLECTED assert result.ret == EXIT_NOTESTSCOLLECTED
@pytest.mark.filterwarnings(
"ignore:usage of Generator.Function is deprecated, please use pytest.Function instead"
)
class TestGenerator(object): class TestGenerator(object):
def test_generative_functions(self, testdir): def test_generative_functions(self, testdir):
modcol = testdir.getmodulecol( modcol = testdir.getmodulecol(
@ -1255,6 +1258,9 @@ class TestReportInfo(object):
assert lineno == 1 assert lineno == 1
assert msg == "TestClass" assert msg == "TestClass"
@pytest.mark.filterwarnings(
"ignore:usage of Generator.Function is deprecated, please use pytest.Function instead"
)
def test_generator_reportinfo(self, testdir): def test_generator_reportinfo(self, testdir):
modcol = testdir.getmodulecol( modcol = testdir.getmodulecol(
""" """

View File

@ -6,7 +6,7 @@ import pytest
from _pytest.pytester import get_public_names from _pytest.pytester import get_public_names
from _pytest.fixtures import FixtureLookupError, FixtureRequest from _pytest.fixtures import FixtureLookupError, FixtureRequest
from _pytest import fixtures from _pytest import fixtures
from _pytest.compat import Path from _pytest.pathlib import Path
def test_getfuncargnames(): def test_getfuncargnames():
@ -993,6 +993,7 @@ class TestRequestCachedSetup(object):
) )
reprec.assertoutcome(passed=4) reprec.assertoutcome(passed=4)
@pytest.mark.filterwarnings("ignore:cached_setup is deprecated")
def test_request_cachedsetup_extrakey(self, testdir): def test_request_cachedsetup_extrakey(self, testdir):
item1 = testdir.getitem("def test_func(): pass") item1 = testdir.getitem("def test_func(): pass")
req1 = fixtures.FixtureRequest(item1) req1 = fixtures.FixtureRequest(item1)
@ -1010,6 +1011,7 @@ class TestRequestCachedSetup(object):
assert ret1 == ret1b assert ret1 == ret1b
assert ret2 == ret2b assert ret2 == ret2b
@pytest.mark.filterwarnings("ignore:cached_setup is deprecated")
def test_request_cachedsetup_cache_deletion(self, testdir): def test_request_cachedsetup_cache_deletion(self, testdir):
item1 = testdir.getitem("def test_func(): pass") item1 = testdir.getitem("def test_func(): pass")
req1 = fixtures.FixtureRequest(item1) req1 = fixtures.FixtureRequest(item1)
@ -1221,8 +1223,7 @@ class TestFixtureUsages(object):
result = testdir.runpytest_inprocess() result = testdir.runpytest_inprocess()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
( (
"*ValueError: fixture badscope from test_invalid_scope.py has an unsupported" "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'"
" scope value 'functions'"
) )
) )
@ -3611,16 +3612,15 @@ class TestParameterizedSubRequest(object):
) )
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" [
E*Failed: The requested fixture has no parameter defined for test: "The requested fixture has no parameter defined for test:",
E* test_call_from_fixture.py::test_foo " test_call_from_fixture.py::test_foo",
E* "Requested fixture 'fix_with_param' defined in:",
E*Requested fixture 'fix_with_param' defined in: "test_call_from_fixture.py:4",
E*test_call_from_fixture.py:4 "Requested here:",
E*Requested here: "test_call_from_fixture.py:9",
E*test_call_from_fixture.py:9 "*1 error in*",
*1 error* ]
"""
) )
def test_call_from_test(self, testdir): def test_call_from_test(self, testdir):
@ -3638,16 +3638,15 @@ class TestParameterizedSubRequest(object):
) )
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" [
E*Failed: The requested fixture has no parameter defined for test: "The requested fixture has no parameter defined for test:",
E* test_call_from_test.py::test_foo " test_call_from_test.py::test_foo",
E* "Requested fixture 'fix_with_param' defined in:",
E*Requested fixture 'fix_with_param' defined in: "test_call_from_test.py:4",
E*test_call_from_test.py:4 "Requested here:",
E*Requested here: "test_call_from_test.py:8",
E*test_call_from_test.py:8 "*1 failed*",
*1 failed* ]
"""
) )
def test_external_fixture(self, testdir): def test_external_fixture(self, testdir):
@ -3669,16 +3668,16 @@ class TestParameterizedSubRequest(object):
) )
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" [
E*Failed: The requested fixture has no parameter defined for test: "The requested fixture has no parameter defined for test:",
E* test_external_fixture.py::test_foo " test_external_fixture.py::test_foo",
E* "",
E*Requested fixture 'fix_with_param' defined in: "Requested fixture 'fix_with_param' defined in:",
E*conftest.py:4 "conftest.py:4",
E*Requested here: "Requested here:",
E*test_external_fixture.py:2 "test_external_fixture.py:2",
*1 failed* "*1 failed*",
""" ]
) )
def test_non_relative_path(self, testdir): def test_non_relative_path(self, testdir):
@ -3713,16 +3712,16 @@ class TestParameterizedSubRequest(object):
testdir.syspathinsert(fixdir) testdir.syspathinsert(fixdir)
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
""" [
E*Failed: The requested fixture has no parameter defined for test: "The requested fixture has no parameter defined for test:",
E* test_foos.py::test_foo " test_foos.py::test_foo",
E* "",
E*Requested fixture 'fix_with_param' defined in: "Requested fixture 'fix_with_param' defined in:",
E*fix.py:4 "*fix.py:4",
E*Requested here: "Requested here:",
E*test_foos.py:4 "test_foos.py:4",
*1 failed* "*1 failed*",
""" ]
) )

View File

@ -127,10 +127,11 @@ class TestMetafunc(object):
pass pass
metafunc = self.Metafunc(func) 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") 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): def test_find_parametrized_scope(self):
"""unittest for _find_parametrized_scope (#3941)""" """unittest for _find_parametrized_scope (#3941)"""
@ -206,16 +207,13 @@ class TestMetafunc(object):
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)
pytest.raises( with pytest.raises(pytest.fail.Exception):
ValueError, lambda: metafunc.parametrize("x", [1, 2], ids=["basic"]) metafunc.parametrize("x", [1, 2], ids=["basic"])
)
pytest.raises( with pytest.raises(pytest.fail.Exception):
ValueError, metafunc.parametrize(
lambda: metafunc.parametrize(
("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"] ("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"]
), )
)
@pytest.mark.issue510 @pytest.mark.issue510
def test_parametrize_empty_list(self): def test_parametrize_empty_list(self):
@ -573,7 +571,7 @@ class TestMetafunc(object):
pass pass
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)
with pytest.raises(ValueError): with pytest.raises(pytest.fail.Exception):
metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"]) metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"])
@pytest.mark.issue714 @pytest.mark.issue714
@ -1189,7 +1187,9 @@ class TestMetafuncFunctional(object):
) )
result = testdir.runpytest() result = testdir.runpytest()
result.stdout.fnmatch_lines( 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): def test_parametrize_with_identical_ids_get_unique_names(self, testdir):
@ -1326,13 +1326,13 @@ class TestMetafuncFunctional(object):
attr attr
) )
) )
reprec = testdir.inline_run("--collectonly") result = testdir.runpytest("--collectonly")
failures = reprec.getfailures() result.stdout.fnmatch_lines(
assert len(failures) == 1 [
expectederror = "MarkerError: test_foo has '{}', spelling should be 'parametrize'".format( "test_foo has '{}' mark, spelling should be 'parametrize'".format(attr),
attr "*1 error in*",
]
) )
assert expectederror in failures[0].longrepr.reprcrash.message
class TestMetafuncFunctionalAuto(object): class TestMetafuncFunctionalAuto(object):

View File

@ -4,7 +4,7 @@ import textwrap
import py import py
import pytest import pytest
from _pytest.config import PytestPluginManager 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"]) @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() 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): def test_no_conftest(testdir):
testdir.makeconftest("assert 0") testdir.makeconftest("assert 0")
result = testdir.runpytest("--noconftest") result = testdir.runpytest("--noconftest")

View File

@ -13,7 +13,7 @@ from _pytest.mark import (
transfer_markers, transfer_markers,
EMPTY_PARAMETERSET_OPTION, EMPTY_PARAMETERSET_OPTION,
) )
from _pytest.nodes import Node from _pytest.nodes import Node, Collector
ignore_markinfo = pytest.mark.filterwarnings( ignore_markinfo = pytest.mark.filterwarnings(
"ignore:MarkInfo objects:pytest.RemovedInPytest4Warning" "ignore:MarkInfo objects:pytest.RemovedInPytest4Warning"
@ -247,7 +247,7 @@ def test_marker_without_description(testdir):
) )
ftdir = testdir.mkdir("ft1_dummy") ftdir = testdir.mkdir("ft1_dummy")
testdir.tmpdir.join("conftest.py").move(ftdir.join("conftest.py")) testdir.tmpdir.join("conftest.py").move(ftdir.join("conftest.py"))
rec = testdir.runpytest_subprocess("--strict") rec = testdir.runpytest("--strict")
rec.assert_outcomes() rec.assert_outcomes()
@ -302,7 +302,7 @@ def test_strict_prohibits_unregistered_markers(testdir):
) )
result = testdir.runpytest("--strict") result = testdir.runpytest("--strict")
assert result.ret != 0 assert result.ret != 0
result.stdout.fnmatch_lines(["*unregisteredmark*not*registered*"]) result.stdout.fnmatch_lines(["'unregisteredmark' not a registered marker"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1103,7 +1103,14 @@ class TestMarkDecorator(object):
@pytest.mark.parametrize("mark", [None, "", "skip", "xfail"]) @pytest.mark.parametrize("mark", [None, "", "skip", "xfail"])
def test_parameterset_for_parametrize_marks(testdir, mark): def test_parameterset_for_parametrize_marks(testdir, mark):
if mark is not None: 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() config = testdir.parseconfig()
from _pytest.mark import pytest_configure, get_empty_parameterset_mark 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 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): def test_parameterset_for_parametrize_bad_markname(testdir):
with pytest.raises(pytest.UsageError): with pytest.raises(pytest.UsageError):
test_parameterset_for_parametrize_marks(testdir, "bad") test_parameterset_for_parametrize_marks(testdir, "bad")

View File

@ -4,7 +4,7 @@ import py
import pytest import pytest
from _pytest.paths import fnmatch_ex from _pytest.pathlib import fnmatch_ex
class TestPort: class TestPort:

View File

@ -4,6 +4,7 @@ import os
import py.path import py.path
import pytest import pytest
import sys import sys
import time
import _pytest.pytester as pytester import _pytest.pytester as pytester
from _pytest.pytester import HookRecorder from _pytest.pytester import HookRecorder
from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot
@ -401,3 +402,34 @@ def test_testdir_subprocess(testdir):
def test_unicode_args(testdir): def test_unicode_args(testdir):
result = testdir.runpytest("-k", u"💩") result = testdir.runpytest("-k", u"💩")
assert result.ret == EXIT_NOTESTSCOLLECTED 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)

View File

@ -76,9 +76,8 @@ class TestDeprecatedCall(object):
) )
def test_deprecated_call_raises(self): 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) pytest.deprecated_call(self.dep, 3, 5)
assert "Did not produce" in str(excinfo)
def test_deprecated_call(self): def test_deprecated_call(self):
pytest.deprecated_call(self.dep, 0, 5) pytest.deprecated_call(self.dep, 0, 5)
@ -100,7 +99,7 @@ class TestDeprecatedCall(object):
assert warn_explicit is warnings.warn_explicit assert warn_explicit is warnings.warn_explicit
def test_deprecated_explicit_call_raises(self): def test_deprecated_explicit_call_raises(self):
with pytest.raises(AssertionError): with pytest.raises(pytest.fail.Exception):
pytest.deprecated_call(self.dep_explicit, 3) pytest.deprecated_call(self.dep_explicit, 3)
def test_deprecated_explicit_call(self): def test_deprecated_explicit_call(self):
@ -116,8 +115,8 @@ class TestDeprecatedCall(object):
def f(): def f():
pass pass
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning" msg = "No warnings of type (.*DeprecationWarning.*, .*PendingDeprecationWarning.*)"
with pytest.raises(AssertionError, match=msg): with pytest.raises(pytest.fail.Exception, match=msg):
if mode == "call": if mode == "call":
pytest.deprecated_call(f) pytest.deprecated_call(f)
else: else:
@ -179,12 +178,20 @@ class TestDeprecatedCall(object):
def f(): def f():
warnings.warn(warning("hi")) warnings.warn(warning("hi"))
with pytest.raises(AssertionError): with pytest.raises(pytest.fail.Exception):
pytest.deprecated_call(f) pytest.deprecated_call(f)
with pytest.raises(AssertionError): with pytest.raises(pytest.fail.Exception):
with pytest.deprecated_call(): with pytest.deprecated_call():
f() 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): class TestWarns(object):
def test_strings(self): def test_strings(self):
@ -343,3 +350,13 @@ class TestWarns(object):
with pytest.warns(UserWarning, match=r"aaa"): with pytest.warns(UserWarning, match=r"aaa"):
warnings.warn("bbbbbbbbbb", UserWarning) warnings.warn("bbbbbbbbbb", UserWarning)
warnings.warn("cccccccccc", 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

View File

@ -570,7 +570,20 @@ def test_pytest_exit_msg(testdir):
result.stderr.fnmatch_lines(["Exit: oh noes"]) 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( testdir.makepyfile(
""" """
import pytest import pytest
@ -585,6 +598,21 @@ def test_pytest_fail_notrace(testdir):
assert "def teardown_function" not in result.stdout.str() 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", ""]) @pytest.mark.parametrize("str_prefix", ["u", ""])
def test_pytest_fail_notrace_non_ascii(testdir, str_prefix): def test_pytest_fail_notrace_non_ascii(testdir, str_prefix):
"""Fix pytest.fail with pytrace=False with non-ascii characters (#1178). """Fix pytest.fail with pytrace=False with non-ascii characters (#1178).

View File

@ -154,7 +154,7 @@ class TestTerminal(object):
) )
result = testdir.runpytest(p2) result = testdir.runpytest(p2)
result.stdout.fnmatch_lines(["*test_p2.py .*", "*1 passed*"]) result.stdout.fnmatch_lines(["*test_p2.py .*", "*1 passed*"])
result = testdir.runpytest("-v", p2) result = testdir.runpytest("-vv", p2)
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
["*test_p2.py::TestMore::test_p1* <- *test_p1.py*PASSED*"] ["*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 assert result.ret == 0
result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"]) result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"])
assert " <- " not in result.stdout.str() assert " <- " not in result.stdout.str()

View File

@ -1,7 +1,10 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys import sys
import py
import six
import pytest import pytest
from _pytest.pathlib import Path
def test_tmpdir_fixture(testdir): def test_tmpdir_fixture(testdir):
@ -19,11 +22,11 @@ def test_ensuretemp(recwarn):
class TestTempdirHandler(object): class TestTempdirHandler(object):
def test_mktemp(self, testdir): def test_mktemp(self, testdir):
from _pytest.tmpdir import TempdirFactory from _pytest.tmpdir import TempdirFactory, TempPathFactory
config = testdir.parseconfig() config = testdir.parseconfig()
config.option.basetemp = testdir.mkdir("hello") config.option.basetemp = testdir.mkdir("hello")
t = TempdirFactory(config) t = TempdirFactory(TempPathFactory.from_config(config))
tmp = t.mktemp("world") tmp = t.mktemp("world")
assert tmp.relto(t.getbasetemp()) == "world0" assert tmp.relto(t.getbasetemp()) == "world0"
tmp = t.mktemp("this") tmp = t.mktemp("this")
@ -65,10 +68,6 @@ def test_basetemp(testdir):
assert mytemp.join("hello").check() 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): def test_tmpdir_always_is_realpath(testdir):
# the reason why tmpdir should be a realpath is that # the reason why tmpdir should be a realpath is that
# when you cd to it and do "os.getcwd()" you will anyway # 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"] # os.environ["PWD"]
realtemp = testdir.tmpdir.mkdir("myrealtemp") realtemp = testdir.tmpdir.mkdir("myrealtemp")
linktemp = testdir.tmpdir.join("symlinktemp") linktemp = testdir.tmpdir.join("symlinktemp")
linktemp.mksymlinkto(realtemp) attempt_symlink_to(linktemp, str(realtemp))
p = testdir.makepyfile( p = testdir.makepyfile(
""" """
def test_1(tmpdir): def test_1(tmpdir):
@ -111,7 +110,7 @@ def test_tmpdir_factory(testdir):
def session_dir(tmpdir_factory): def session_dir(tmpdir_factory):
return tmpdir_factory.mktemp('data', numbered=False) return tmpdir_factory.mktemp('data', numbered=False)
def test_some(session_dir): def test_some(session_dir):
session_dir.isdir() assert session_dir.isdir()
""" """
) )
reprec = testdir.inline_run() reprec = testdir.inline_run()
@ -184,3 +183,113 @@ def test_get_user(monkeypatch):
monkeypatch.delenv("USER", raising=False) monkeypatch.delenv("USER", raising=False)
monkeypatch.delenv("USERNAME", raising=False) monkeypatch.delenv("USERNAME", raising=False)
assert get_user() is None 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")

View File

@ -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: class TestDeprecationWarningsByDefault:
""" """
Note: all pytest runs are executed in a subprocess so we don't inherit warning filters 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) self.create_file(testdir)
if customize_filters:
testdir.makeini(
"""
[pytest]
filterwarnings =
once::UserWarning
"""
)
result = testdir.runpytest_subprocess() result = testdir.runpytest_subprocess()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
[ [
@ -468,7 +522,9 @@ class TestDeprecationWarningsByDefault:
testdir.makeini( testdir.makeini(
""" """
[pytest] [pytest]
filterwarnings = once::UserWarning filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
""" """
) )
result = testdir.runpytest_subprocess() result = testdir.runpytest_subprocess()
@ -479,7 +535,8 @@ class TestDeprecationWarningsByDefault:
be displayed normally. be displayed normally.
""" """
self.create_file( self.create_file(
testdir, mark='@pytest.mark.filterwarnings("once::UserWarning")' testdir,
mark='@pytest.mark.filterwarnings("ignore::PendingDeprecationWarning")',
) )
result = testdir.runpytest_subprocess() result = testdir.runpytest_subprocess()
result.stdout.fnmatch_lines( result.stdout.fnmatch_lines(
@ -492,7 +549,12 @@ class TestDeprecationWarningsByDefault:
def test_hidden_by_cmdline(self, testdir): def test_hidden_by_cmdline(self, testdir):
self.create_file(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() assert WARNINGS_SUMMARY_HEADER not in result.stdout.str()
def test_hidden_by_system(self, testdir, monkeypatch): def test_hidden_by_system(self, testdir, monkeypatch):

View File

@ -194,13 +194,14 @@ commands = python scripts/release.py {posargs}
[pytest] [pytest]
minversion = 2.0 minversion = 2.0
plugins = pytester addopts = -ra -p pytester
addopts = -ra -p pytester --ignore=testing/cx_freeze rsyncdirs = tox.ini doc src testing
rsyncdirs = tox.ini pytest.py _pytest testing
python_files = test_*.py *_test.py testing/*/*.py python_files = test_*.py *_test.py testing/*/*.py
python_classes = Test Acceptance python_classes = Test Acceptance
python_functions = test 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 xfail_strict=true
filterwarnings = filterwarnings =
error error