diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fecf24353..e5cc56230 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,9 @@ repos: - id: check-yaml - id: debug-statements exclude: _pytest/debugging.py + language_version: python3 - id: flake8 + language_version: python3 - repo: https://github.com/asottile/pyupgrade rev: v1.8.0 hooks: @@ -41,6 +43,6 @@ repos: - id: changelogs-rst name: changelog filenames language: fail - entry: 'changelog files must be named ####.(feature|bugfix|doc|removal|vendor|trivial).rst' - exclude: changelog/(\d+\.(feature|bugfix|doc|removal|vendor|trivial).rst|README.rst|_template.rst) + entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst' + exclude: changelog/(\d+\.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst) files: ^changelog/ diff --git a/AUTHORS b/AUTHORS index 463810bd9..c63c0a005 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,6 +14,7 @@ Allan Feldman Anatoly Bubenkoff Anders Hovmöller Andras Tim +Andrea Cimatoribus Andreas Zeidler Andrzej Ostrowski Andy Freeland @@ -120,6 +121,7 @@ Katerina Koukiou Kevin Cox Kodi B. Arfer Kostis Anagnostopoulos +Kyle Altendorf Lawrence Mitchell Lee Kamentsky Lev Maximov diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 26bc28af1..015f9dbd3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,163 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 3.9.1 (2018-10-16) +========================= + +Features +-------- + +- `#4159 `_: 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 `_: 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 `_. + + * 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 `_: Add a Deprecation warning for pytest.ensuretemp as it was deprecated since a while. + + + +Features +-------- + +- `#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 `_: 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 `_: Add ``empty_parameter_set_mark=fail_at_collect`` ini option for raising an exception when parametrize collects an empty set. + + +- `#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 `_: Introduce ``tmp_path`` as a fixture providing a Path object. + + +- `#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 `_: Allow specification of timeout for ``Testdir.runpytest_subprocess()`` and ``Testdir.run()``. + + +- `#4098 `_: Add returncode argument to pytest.exit() to exit pytest with a specific return code. + + +- `#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 `_: Require setuptools>=30.3 and move most of the metadata to ``setup.cfg``. + + + +Bug Fixes +--------- + +- `#2535 `_: Improve error message when test functions of ``unittest.TestCase`` subclasses use a parametrized fixture. + + +- `#3057 `_: ``request.fixturenames`` now correctly returns the name of fixtures created by ``request.getfixturevalue()``. + + +- `#3946 `_: Warning filters passed as command line options using ``-W`` now take precedence over filters defined in ``ini`` + configuration files. + + +- `#4066 `_: Fix source reindenting by using ``textwrap.dedent`` directly. + + +- `#4102 `_: ``pytest.warn`` will capture previously-warned warnings in Python 2. Previously they were never raised. + + +- `#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 `_: Fix duplicate printing of internal errors when using ``--pdb``. + + +- `#4135 `_: pathlib based tmpdir cleanup now correctly handles symlinks in the folder. + + +- `#4152 `_: Display the filename when encountering ``SyntaxWarning``. + + + +Improved Documentation +---------------------- + +- `#3713 `_: Update usefixtures documentation to clarify that it can't be used with fixture functions. + + +- `#4058 `_: Update fixture documentation to specify that a fixture can be invoked twice in the scope it's defined for. + + +- `#4064 `_: According to unittest.rst, setUpModule and tearDownModule were not implemented, but it turns out they are. So updated the documentation for unittest. + + +- `#4151 `_: Add tempir testing example to CONTRIBUTING.rst guide + + + +Trivial/Internal Changes +------------------------ + +- `#2293 `_: The internal ``MarkerError`` exception has been removed. + + +- `#3988 `_: Port the implementation of tmpdir to pathlib. + + +- `#4063 `_: Exclude 0.00 second entries from ``--duration`` output unless ``-vv`` is passed on the command-line. + + +- `#4093 `_: Fixed formatting of string literals in internal tests. + + pytest 3.8.2 (2018-10-02) ========================= diff --git a/changelog/2535.bugfix.rst b/changelog/2535.bugfix.rst deleted file mode 100644 index ec16e81ea..000000000 --- a/changelog/2535.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Improve error message when test functions of ``unittest.TestCase`` subclasses use a parametrized fixture. diff --git a/changelog/3057.bugfix.rst b/changelog/3057.bugfix.rst deleted file mode 100644 index 8cc22f278..000000000 --- a/changelog/3057.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``request.fixturenames`` now correctly returns the name of fixtures created by ``request.getfixturevalue()``. diff --git a/changelog/3713.doc.rst b/changelog/3713.doc.rst deleted file mode 100644 index c1d6b51b8..000000000 --- a/changelog/3713.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Update usefixtures documentation to clarify that it can't be used with fixture functions. diff --git a/changelog/4058.doc.rst b/changelog/4058.doc.rst deleted file mode 100644 index 51d568f54..000000000 --- a/changelog/4058.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Update fixture documentation to specify that a fixture can be invoked twice in the scope it's defined for. diff --git a/changelog/4063.trivial.rst b/changelog/4063.trivial.rst deleted file mode 100644 index 465356401..000000000 --- a/changelog/4063.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Exclude 0.00 second entries from ``--duration`` output unless ``-vv`` is passed on the command-line. diff --git a/changelog/4064.doc.rst b/changelog/4064.doc.rst deleted file mode 100644 index 7b34fe43e..000000000 --- a/changelog/4064.doc.rst +++ /dev/null @@ -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. diff --git a/changelog/4066.bugfix.rst b/changelog/4066.bugfix.rst deleted file mode 100644 index 64980d6e8..000000000 --- a/changelog/4066.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix source reindenting by using ``textwrap.dedent`` directly. diff --git a/changelog/4093.trivial.rst b/changelog/4093.trivial.rst deleted file mode 100644 index cbfbeb00d..000000000 --- a/changelog/4093.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed formatting of string literals in internal tests. diff --git a/changelog/4132.bugfix.rst b/changelog/4132.bugfix.rst deleted file mode 100644 index 1fbb9afad..000000000 --- a/changelog/4132.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix duplicate printing of internal errors when using ``--pdb``. diff --git a/changelog/4151.doc.rst b/changelog/4151.doc.rst deleted file mode 100644 index da561002a..000000000 --- a/changelog/4151.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add tempir testing example to CONTRIBUTING.rst guide diff --git a/changelog/4152.bugfix.rst b/changelog/4152.bugfix.rst deleted file mode 100644 index ce2547294..000000000 --- a/changelog/4152.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Display the filename when encountering ``SyntaxWarning``. diff --git a/changelog/README.rst b/changelog/README.rst index 47e21fb33..e471409b0 100644 --- a/changelog/README.rst +++ b/changelog/README.rst @@ -14,7 +14,8 @@ Each file should be named like ``..rst``, where * ``feature``: new user facing features, like new command-line options and new behavior. * ``bugfix``: fixes a reported bug. * ``doc``: documentation improvement, like rewording an entire session or adding missing docs. -* ``removal``: feature deprecation or removal. +* ``deprecation``: feature deprecation. +* ``removal``: feature removal. * ``vendor``: changes in packages vendored in pytest. * ``trivial``: fixing a small typo or internal change that might be noteworthy. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index e6c712120..a692eee15 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,8 @@ Release announcements :maxdepth: 2 + release-3.9.1 + release-3.9.0 release-3.8.2 release-3.8.1 release-3.8.0 diff --git a/doc/en/announce/release-3.9.0.rst b/doc/en/announce/release-3.9.0.rst new file mode 100644 index 000000000..14cfbe903 --- /dev/null +++ b/doc/en/announce/release-3.9.0.rst @@ -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 diff --git a/doc/en/announce/release-3.9.1.rst b/doc/en/announce/release-3.9.1.rst new file mode 100644 index 000000000..f050e4653 --- /dev/null +++ b/doc/en/announce/release-3.9.1.rst @@ -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 diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index e52151a1b..f921b5d64 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -104,7 +104,9 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a See http://docs.python.org/library/warnings.html for information on warning categories. tmpdir_factory - Return a TempdirFactory instance for the test session. + Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. + tmp_path_factory + Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. tmpdir Return a temporary directory path object which is unique to each test function invocation, @@ -113,6 +115,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a path object. .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html + tmp_path + Return a temporary directory path object + which is unique to each test function invocation, + created as a sub directory of the base temporary + directory. The returned object is a :class:`pathlib.Path` + object. + + .. note:: + + in python < 3.6 this is a pathlib2.Path no tests ran in 0.12 seconds diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 14eeeb08f..30746d035 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -14,6 +14,67 @@ Below is a complete list of all pytest features which are considered deprecated. :class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. +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 `_ 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`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 7ec27d547..df83ec97e 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -574,7 +574,7 @@ We can run this:: file $REGENDOC_TMPDIR/b/test_error.py, line 1 def test_root(db): # no db here, will error out E fixture 'db' not found - > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, record_xml_property, recwarn, tmpdir, tmpdir_factory + > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_xml_attribute, record_xml_property, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory > use 'pytest --fixtures [testpath]' for help on them. $REGENDOC_TMPDIR/b/test_error.py:1 diff --git a/doc/en/historical-notes.rst b/doc/en/historical-notes.rst index 028ceff9b..9462d700f 100644 --- a/doc/en/historical-notes.rst +++ b/doc/en/historical-notes.rst @@ -175,3 +175,13 @@ Previous to version 2.4 to set a break point in code one needed to use ``pytest. This is no longer needed and one can use the native ``import pdb;pdb.set_trace()`` call directly. For more details see :ref:`breakpoints`. + +"compat" properties +------------------- + +.. deprecated:: 3.9 + +Access of ``Module``, ``Function``, ``Class``, ``Instance``, ``File`` and ``Item`` through ``Node`` instances have long +been documented as deprecated, but started to emit warnings from pytest ``3.9`` and onward. + +Users should just ``import pytest`` and access those objects using the ``pytest`` module. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 1537efc61..632bb4e36 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -982,6 +982,7 @@ passed multiple times. The expected format is ``name=value``. For example:: * ``skip`` skips tests with an empty parameterset (default) * ``xfail`` marks tests with an empty parameterset as xfail(run=False) + * ``fail_at_collect`` raises an exception if parametrize collects an empty parameter set .. code-block:: ini diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 421b4c898..d8cd8b705 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -5,6 +5,76 @@ Temporary directories and files ================================================ +The ``tmp_path`` fixture +------------------------ + +.. versionadded:: 3.9 + + +You can use the ``tmpdir`` fixture which will +provide a temporary directory unique to the test invocation, +created in the `base temporary directory`_. + +``tmpdir`` is a ``pathlib/pathlib2.Path`` object. Here is an example test usage: + +.. code-block:: python + + # content of test_tmp_path.py + import os + + CONTENT = u"content" + + + def test_create_file(tmp_path): + d = tmp_path / "sub" + d.mkdir() + p = d / "hello.txt" + p.write_text(CONTENT) + assert p.read_text() == CONTENT + assert len(list(tmp_path.iterdir())) == 1 + assert 0 + +Running this would result in a passed test except for the last +``assert 0`` line which we use to look at values:: + + $ pytest test_tmp_path.py + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y + rootdir: $REGENDOC_TMPDIR, inifile: + collected 1 item + + test_tmp_path.py F [100%] + + ================================= FAILURES ================================= + _____________________________ test_create_file _____________________________ + + tmp_path = PosixPath('PYTEST_TMPDIR/test_create_file0') + + def test_create_file(tmp_path): + d = tmp_path / "sub" + d.mkdir() + p = d / "hello.txt" + p.write_text(CONTENT) + assert p.read_text() == CONTENT + assert len(list(tmp_path.iterdir())) == 1 + > assert 0 + E assert 0 + + test_tmp_path.py:13: AssertionError + ========================= 1 failed in 0.12 seconds ========================= + +The ``tmp_path_factory`` fixture +-------------------------------- + +.. versionadded:: 3.9 + + +The ``tmp_path_facotry`` is a session-scoped fixture which can be used +to create arbitrary temporary directories from any other fixture or test. + +its intended to replace ``tmpdir_factory`` and returns :class:`pathlib.Path` instances. + + The 'tmpdir' fixture -------------------- diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 9c28ecb49..060057d91 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -101,22 +101,28 @@ DeprecationWarning and PendingDeprecationWarning ------------------------------------------------ .. versionadded:: 3.8 +.. versionchanged:: 3.9 -By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` if no other warning filters -are configured. +By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning``. -To disable showing ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings, you might define any warnings -filter either in the command-line or in the ini file, or you can use: +Sometimes it is useful to hide some specific deprecation warnings that happen in code that you have no control over +(such as third-party libraries), in which case you might use the standard warning filters options (ini or marks). +For example: .. code-block:: ini [pytest] filterwarnings = - ignore::DeprecationWarning - ignore::PendingDeprecationWarning + ignore:.*U.*mode is deprecated:DeprecationWarning + .. note:: - This makes pytest more compliant with `PEP-0506 `_ which suggests that those warnings should + If warnings are configured at the interpreter level, using + the `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 `_ which suggests that those warnings should be shown by default by test runners, but pytest doesn't follow ``PEP-0506`` completely because resetting all warning filters like suggested in the PEP will break existing test suites that configure warning filters themselves by calling ``warnings.simplefilter`` (see issue `#2430 `_ diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 70e48f817..2cb1caefb 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -420,9 +420,21 @@ additionally it is possible to copy examples for a example folder before running ============================= warnings summary ============================= $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time testdir.copy_example("test_example.py") + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Class is deprecated, please use pytest.Class instead + return getattr(object, name, default) + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.File is deprecated, please use pytest.File instead + return getattr(object, name, default) + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Function is deprecated, please use pytest.Function instead + return getattr(object, name, default) + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Instance is deprecated, please use pytest.Instance instead + return getattr(object, name, default) + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Item is deprecated, please use pytest.Item instead + return getattr(object, name, default) + $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:321: RemovedInPytest4Warning: usage of Session.Module is deprecated, please use pytest.Module instead + return getattr(object, name, default) -- Docs: https://docs.pytest.org/en/latest/warnings.html - =================== 2 passed, 1 warnings in 0.12 seconds =================== + =================== 2 passed, 7 warnings in 0.12 seconds =================== For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult diff --git a/pyproject.toml b/pyproject.toml index b1e85601e..c83bd853d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [build-system] requires = [ - "setuptools", + # sync with setup.py until we discard non-pep-517/518 + "setuptools>=30.3", "setuptools-scm", "wheel", ] @@ -15,7 +16,12 @@ template = "changelog/_template.rst" [[tool.towncrier.type]] directory = "removal" - name = "Deprecations and Removals" + name = "Removals" + showcontent = true + + [[tool.towncrier.type]] + directory = "deprecation" + name = "Deprecations" showcontent = true [[tool.towncrier.type]] diff --git a/setup.cfg b/setup.cfg index 816539e2e..8cd3858fd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,55 @@ +[metadata] + +name = pytest +description = pytest: simple powerful testing with Python +long_description = file: README.rst +url = https://docs.pytest.org/en/latest/ +project_urls = + Source=https://github.com/pytest-dev/pytest + Tracker=https://github.com/pytest-dev/pytest/issues + +author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others + +license = MIT license +license_file = LICENSE +keywords = test, unittest +classifiers = + Development Status :: 6 - Mature + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: POSIX + Operating System :: Microsoft :: Windows + Operating System :: MacOS :: MacOS X + Topic :: Software Development :: Testing + Topic :: Software Development :: Libraries + Topic :: Utilities + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 +platforms = unix, linux, osx, cygwin, win32 + +[options] +zip_safe = no +packages = + _pytest + _pytest.assertion + _pytest._code + _pytest.mark + _pytest.config + +py_modules = pytest +python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* + + +[options.entry_points] +console_scripts = + pytest=pytest:main + py.test=pytest:main + [build_sphinx] source-dir = doc/en/ build-dir = doc/build @@ -13,8 +65,6 @@ universal = 1 ignore = _pytest/_version.py -[metadata] -license_file = LICENSE [devpi:upload] formats = sdist.tgz,bdist_wheel diff --git a/setup.py b/setup.py index 4c12fbfcc..6bab5312d 100644 --- a/setup.py +++ b/setup.py @@ -1,126 +1,34 @@ import os -import sys -import setuptools -import pkg_resources from setuptools import setup -classifiers = [ - "Development Status :: 6 - Mature", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: POSIX", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS :: MacOS X", - "Topic :: Software Development :: Testing", - "Topic :: Software Development :: Libraries", - "Topic :: Utilities", -] + [ - ("Programming Language :: Python :: %s" % x) - for x in "2 2.7 3 3.4 3.5 3.6 3.7".split() + +# TODO: if py gets upgrade to >=1.6, +# remove _width_of_current_line in terminal.py +INSTALL_REQUIRES = [ + "py>=1.5.0", + "six>=1.10.0", + "setuptools", + "attrs>=17.4.0", + "more-itertools>=4.0.0", + "atomicwrites>=1.0", + 'funcsigs;python_version<"3.0"', + 'pathlib2>=2.2.0;python_version<"3.6"', + 'colorama;sys_platform=="win32"', ] -with open("README.rst") as fd: - long_description = fd.read() - -def get_environment_marker_support_level(): - """ - Tests how well setuptools supports PEP-426 environment marker. - - The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2 - so we're using that), see: https://setuptools.readthedocs.io/en/latest/history.html#id350 - - The support is later enhanced to allow direct conditional inclusions inside install_requires, - which is now recommended by setuptools. It first appeared in 36.2.0, went broken with 36.2.1, and - again worked since 36.2.2, so we're using that. See: - https://setuptools.readthedocs.io/en/latest/history.html#v36-2-2 - https://github.com/pypa/setuptools/issues/1099 - - References: - - * https://wheel.readthedocs.io/en/latest/index.html#defining-conditional-dependencies - * https://www.python.org/dev/peps/pep-0426/#environment-markers - * https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies - """ - try: - version = pkg_resources.parse_version(setuptools.__version__) - if version >= pkg_resources.parse_version("36.2.2"): - return 2 - if version >= pkg_resources.parse_version("0.7.2"): - return 1 - except Exception as exc: - sys.stderr.write("Could not test setuptool's version: %s\n" % exc) - - # as of testing on 2018-05-26 fedora was on version 37* and debian was on version 33+ - # we should consider erroring on those - return 0 +# 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") 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( - name="pytest", - description="pytest: simple powerful testing with Python", - long_description=long_description, use_scm_version={"write_to": "src/_pytest/_version.py"}, - url="https://docs.pytest.org/en/latest/", - project_urls={ - "Source": "https://github.com/pytest-dev/pytest", - "Tracker": "https://github.com/pytest-dev/pytest/issues", - }, - license="MIT license", - platforms=["unix", "linux", "osx", "cygwin", "win32"], - author=( - "Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, " - "Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others" - ), - entry_points={"console_scripts": ["pytest=pytest:main", "py.test=pytest:main"]}, - classifiers=classifiers, - keywords="test unittest", - # the following should be enabled for release - setup_requires=["setuptools-scm"], + setup_requires=["setuptools-scm", "setuptools>=30.3"], package_dir={"": "src"}, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", - install_requires=install_requires, - extras_require=extras_require, - packages=[ - "_pytest", - "_pytest.assertion", - "_pytest._code", - "_pytest.mark", - "_pytest.config", - ], - py_modules=["pytest"], - zip_safe=False, + install_requires=INSTALL_REQUIRES, ) diff --git a/src/_pytest/_code/__init__.py b/src/_pytest/_code/__init__.py index 815c13b42..7885d51de 100644 --- a/src/_pytest/_code/__init__.py +++ b/src/_pytest/_code/__init__.py @@ -4,6 +4,7 @@ from .code import Code # noqa from .code import ExceptionInfo # noqa from .code import Frame # noqa from .code import Traceback # noqa +from .code import filter_traceback # noqa from .code import getrawcode # noqa from .source import Source # noqa from .source import compile_ as compile # noqa diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 2662e4320..c0f6d21a2 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -6,8 +6,10 @@ import traceback from inspect import CO_VARARGS, CO_VARKEYWORDS import attr +import pluggy import re from weakref import ref +import _pytest from _pytest.compat import _PY2, _PY3, PY35, safe_str from six import text_type import py @@ -451,13 +453,35 @@ class ExceptionInfo(object): tbfilter=True, funcargs=False, truncate_locals=True, + chain=True, ): - """ return str()able representation of this exception info. - showlocals: show locals per traceback entry - style: long|short|no|native traceback style - tbfilter: hide entries (where __tracebackhide__ is true) + """ + Return str()able representation of this exception info. - in case of style==native, tbfilter and showlocals is ignored. + :param bool showlocals: + Show locals per traceback entry. + Ignored if ``style=="native"``. + + :param str style: long|short|no|native traceback style + + :param bool abspath: + If paths should be changed to absolute or left unchanged. + + :param bool tbfilter: + Hide entries that contain a local variable ``__tracebackhide__==True``. + Ignored if ``style=="native"``. + + :param bool funcargs: + Show fixtures ("funcargs" for legacy purposes) per traceback entry. + + :param bool truncate_locals: + With ``showlocals==True``, make sure locals can be safely represented as strings. + + :param bool chain: if chained exceptions in Python 3 should be shown. + + .. versionchanged:: 3.9 + + Added the ``chain`` parameter. """ if style == "native": return ReprExceptionInfo( @@ -476,6 +500,7 @@ class ExceptionInfo(object): tbfilter=tbfilter, funcargs=funcargs, truncate_locals=truncate_locals, + chain=chain, ) return fmt.repr_excinfo(self) @@ -516,6 +541,7 @@ class FormattedExcinfo(object): tbfilter = attr.ib(default=True) funcargs = attr.ib(default=False) truncate_locals = attr.ib(default=True) + chain = attr.ib(default=True) astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False) def _getindent(self, source): @@ -735,7 +761,7 @@ class FormattedExcinfo(object): reprcrash = None repr_chain += [(reprtraceback, reprcrash, descr)] - if e.__cause__ is not None: + if e.__cause__ is not None and self.chain: e = e.__cause__ excinfo = ( ExceptionInfo((type(e), e, e.__traceback__)) @@ -743,7 +769,11 @@ class FormattedExcinfo(object): else None ) descr = "The above exception was the direct cause of the following exception:" - elif e.__context__ is not None and not e.__suppress_context__: + elif ( + e.__context__ is not None + and not e.__suppress_context__ + and self.chain + ): e = e.__context__ excinfo = ( ExceptionInfo((type(e), e, e.__traceback__)) @@ -979,3 +1009,36 @@ else: return "maximum recursion depth exceeded" in str(excinfo.value) except UnicodeError: return False + + +# relative paths that we use to filter traceback entries from appearing to the user; +# see filter_traceback +# note: if we need to add more paths than what we have now we should probably use a list +# for better maintenance + +_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc")) +# pluggy is either a package or a single module depending on the version +if _PLUGGY_DIR.basename == "__init__.py": + _PLUGGY_DIR = _PLUGGY_DIR.dirpath() +_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath() +_PY_DIR = py.path.local(py.__file__).dirpath() + + +def filter_traceback(entry): + """Return True if a TracebackEntry instance should be removed from tracebacks: + * dynamically generated code (no code to show up for it); + * internal traceback from pytest or its internal libraries, py and pluggy. + """ + # entry.path might sometimes return a str object when the entry + # points to dynamically generated code + # see https://bitbucket.org/pytest-dev/py/issues/71 + raw_filename = entry.frame.code.raw.co_filename + is_generated = "<" in raw_filename and ">" in raw_filename + if is_generated: + return False + # entry.path might point to a non-existing file, in which case it will + # also return a str object. see #1133 + p = py.path.local(entry.path) + return ( + not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR) + ) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 385ae4e77..5e76563d9 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -17,8 +17,9 @@ import atomicwrites import py from _pytest.assertion import util -from _pytest.compat import PurePath, spec_from_file_location -from _pytest.paths import fnmatch_ex +from _pytest.pathlib import PurePath +from _pytest.compat import spec_from_file_location +from _pytest.pathlib import fnmatch_ex # pytest caches rewritten pycs in __pycache__. if hasattr(imp, "get_tag"): diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index dc71303c0..94f42e8cf 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -13,10 +13,9 @@ import attr import pytest import json -import shutil -from . import paths -from .compat import _PY2 as PY2, Path +from .compat import _PY2 as PY2 +from .pathlib import Path, resolve_from_str, rmtree README_CONTENT = u"""\ # pytest cache directory # @@ -39,13 +38,13 @@ class Cache(object): def for_config(cls, config): cachedir = cls.cache_dir_from_config(config) if config.getoption("cacheclear") and cachedir.exists(): - shutil.rmtree(str(cachedir)) + rmtree(cachedir, force=True) cachedir.mkdir() return cls(cachedir, config) @staticmethod def cache_dir_from_config(config): - return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir) + return resolve_from_str(config.getini("cache_dir"), config.rootdir) def warn(self, fmt, **args): from _pytest.warnings import _issue_config_warning diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 6f48cdc3f..0cf0e41c2 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -23,8 +23,6 @@ except ImportError: # pragma: no cover # Only available in Python 3.4+ or as a backport enum = None -__all__ = ["Path", "PurePath"] - _PY3 = sys.version_info > (3, 0) _PY2 = not _PY3 @@ -41,11 +39,6 @@ PY35 = sys.version_info[:2] >= (3, 5) PY36 = sys.version_info[:2] >= (3, 6) MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError" -if PY36: - from pathlib import Path, PurePath -else: - from pathlib2 import Path, PurePath - if _PY3: from collections.abc import MutableMapping as MappingMixin diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 0f8d2d5f3..88cbf14ba 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function import argparse import inspect import shlex -import traceback import types import warnings import copy @@ -19,6 +18,7 @@ import _pytest._code import _pytest.hookspec # the extension point definitions import _pytest.assertion from pluggy import PluginManager, HookimplMarker, HookspecMarker +from _pytest._code import ExceptionInfo, filter_traceback from _pytest.compat import safe_str from .exceptions import UsageError, PrintHelp from .findpaths import determine_setup, exists @@ -26,9 +26,6 @@ from .findpaths import determine_setup, exists hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") -# pytest startup -# - class ConftestImportFailure(Exception): def __init__(self, path, excinfo): @@ -36,12 +33,6 @@ class ConftestImportFailure(Exception): self.path = path self.excinfo = excinfo - def __str__(self): - etype, evalue, etb = self.excinfo - formatted = traceback.format_tb(etb) - # The level of the tracebacks we want to print is hand crafted :( - return repr(evalue) + "\n" + "".join(formatted[2:]) - def main(args=None, plugins=None): """ return exit code, after performing an in-process test run. @@ -57,10 +48,20 @@ def main(args=None, plugins=None): try: config = _prepareconfig(args, plugins) except ConftestImportFailure as e: + exc_info = ExceptionInfo(e.excinfo) tw = py.io.TerminalWriter(sys.stderr) - for line in traceback.format_exception(*e.excinfo): + tw.line( + "ImportError while loading conftest '{e.path}'.".format(e=e), red=True + ) + exc_info.traceback = exc_info.traceback.filter(filter_traceback) + exc_repr = ( + exc_info.getrepr(style="short", chain=False) + if exc_info.traceback + else exc_info.exconly() + ) + formatted_tb = safe_str(exc_repr) + for line in formatted_tb.splitlines(): tw.line(line.rstrip(), red=True) - tw.line("ERROR: could not load %s\n" % (e.path,), red=True) return 4 else: try: @@ -378,25 +379,27 @@ class PytestPluginManager(PluginManager): def _getconftestmodules(self, path): if self._noconftest: return [] - try: - return self._path2confmods[path] - except KeyError: - if path.isfile(): - clist = self._getconftestmodules(path.dirpath()) - 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 def _rget_with_confmod(self, name, path): diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 7480603be..f99430198 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -103,21 +103,18 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): if inifile: iniconfig = py.iniconfig.IniConfig(inifile) is_cfg_file = str(inifile).endswith(".cfg") - # TODO: [pytest] section in *.cfg files is depricated. Need refactoring. sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"] for section in sections: try: inicfg = iniconfig[section] if is_cfg_file and section == "pytest" and config is not None: from _pytest.deprecated import CFG_PYTEST_SECTION - from _pytest.warning_types import RemovedInPytest4Warning from _pytest.warnings import _issue_config_warning + # TODO: [pytest] section in *.cfg files is deprecated. Need refactoring once + # the deprecation expires. _issue_config_warning( - RemovedInPytest4Warning( - CFG_PYTEST_SECTION.format(filename=str(inifile)) - ), - config, + CFG_PYTEST_SECTION.format(filename=str(inifile)), config ) break except KeyError: diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index c26cf4879..54886c999 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -4,10 +4,15 @@ that is planned to be removed in the next pytest release. Keeping it in a central location makes it easy to track what is deprecated and should be removed when the time comes. + +All constants defined in this module should be either PytestWarning instances or UnformattedWarning +in case of warnings which need to format their messages. """ from __future__ import absolute_import, division, print_function -from _pytest.warning_types import RemovedInPytest4Warning + +from _pytest.warning_types import UnformattedWarning, RemovedInPytest4Warning + MAIN_STR_ARGS = RemovedInPytest4Warning( "passing a string to pytest.main() is deprecated, " @@ -18,25 +23,48 @@ YIELD_TESTS = RemovedInPytest4Warning( "yield tests are deprecated, and scheduled to be removed in pytest 4.0" ) -FUNCARG_PREFIX = ( +CACHED_SETUP = RemovedInPytest4Warning( + "cached_setup is deprecated and will be removed in a future release. " + "Use standard fixture functions instead." +) + +COMPAT_PROPERTY = UnformattedWarning( + RemovedInPytest4Warning, + "usage of {owner}.{name} is deprecated, please use pytest.{name} instead", +) + +CUSTOM_CLASS = UnformattedWarning( + RemovedInPytest4Warning, + 'use of special named "{name}" objects in collectors of type "{type_name}" to ' + "customize the created nodes is deprecated. " + "Use pytest_pycollect_makeitem(...) to create custom " + "collection nodes instead.", +) + +FUNCARG_PREFIX = UnformattedWarning( + RemovedInPytest4Warning, '{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated ' "and scheduled to be removed in pytest 4.0. " - "Please remove the prefix and use the @pytest.fixture decorator instead." + "Please remove the prefix and use the @pytest.fixture decorator instead.", ) -FIXTURE_FUNCTION_CALL = ( +FIXTURE_FUNCTION_CALL = UnformattedWarning( + RemovedInPytest4Warning, 'Fixture "{name}" called directly. Fixtures are not meant to be called directly, ' "are created automatically when test functions request them as parameters. " - "See https://docs.pytest.org/en/latest/fixture.html for more information." + "See https://docs.pytest.org/en/latest/fixture.html for more information.", ) -CFG_PYTEST_SECTION = ( - "[pytest] section in {filename} files is deprecated, use [tool:pytest] instead." +CFG_PYTEST_SECTION = UnformattedWarning( + RemovedInPytest4Warning, + "[pytest] section in {filename} files is deprecated, use [tool:pytest] instead.", ) -GETFUNCARGVALUE = "getfuncargvalue is deprecated, use getfixturevalue" +GETFUNCARGVALUE = RemovedInPytest4Warning( + "getfuncargvalue is deprecated, use getfixturevalue" +) -RESULT_LOG = ( +RESULT_LOG = RemovedInPytest4Warning( "--result-log is deprecated and scheduled for removal in pytest 4.0.\n" "See https://docs.pytest.org/en/latest/usage.html#creating-resultlog-format-files for more information." ) @@ -81,3 +109,8 @@ PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = RemovedInPytest4Warning( PYTEST_NAMESPACE = RemovedInPytest4Warning( "pytest_namespace is deprecated and will be removed soon" ) + +PYTEST_ENSURETEMP = RemovedInPytest4Warning( + "pytest/tmpdir_factory.ensuretemp is deprecated, \n" + "please use the tmp_path fixture or tmp_path_factory.mktemp" +) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 21de82b02..29eda351f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -32,7 +32,7 @@ from _pytest.compat import ( get_real_method, _PytestWrapper, ) -from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning +from _pytest.deprecated import FIXTURE_FUNCTION_CALL from _pytest.outcomes import fail, TEST_OUTCOME FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}' @@ -481,6 +481,9 @@ class FixtureRequest(FuncargnamesCompatAttr): or ``session`` indicating the caching lifecycle of the resource. :arg extrakey: added to internal caching key of (funcargname, scope). """ + from _pytest.deprecated import CACHED_SETUP + + warnings.warn(CACHED_SETUP, stacklevel=2) if not hasattr(self.config, "_setupcache"): self.config._setupcache = {} # XXX weakref? cachekey = (self.fixturename, self._getscopeitem(scope), extrakey) @@ -514,7 +517,7 @@ class FixtureRequest(FuncargnamesCompatAttr): """ Deprecated, use getfixturevalue. """ from _pytest import deprecated - warnings.warn(deprecated.GETFUNCARGVALUE, DeprecationWarning, stacklevel=2) + warnings.warn(deprecated.GETFUNCARGVALUE, stacklevel=2) return self.getfixturevalue(argname) def _get_active_fixturedef(self, argname): @@ -576,7 +579,7 @@ class FixtureRequest(FuncargnamesCompatAttr): nodeid=funcitem.nodeid, typename=type(funcitem).__name__, ) - fail(msg) + fail(msg, pytrace=False) if has_params: frame = inspect.stack()[3] frameinfo = inspect.getframeinfo(frame[0]) @@ -597,7 +600,7 @@ class FixtureRequest(FuncargnamesCompatAttr): source_lineno, ) ) - fail(msg) + fail(msg, pytrace=False) else: # indices might not be set if old-style metafunc.addcall() was used param_index = funcitem.callspec.indices.get(argname, 0) @@ -715,10 +718,11 @@ def scope2index(scope, descr, where=None): try: return scopes.index(scope) except ValueError: - raise ValueError( - "{} {}has an unsupported scope value '{}'".format( + fail( + "{} {}got an unexpected scope value '{}'".format( descr, "from {} ".format(where) if where else "", scope - ) + ), + pytrace=False, ) @@ -851,7 +855,9 @@ class FixtureDef(object): self.argname = argname self.scope = scope self.scopenum = scope2index( - scope or "function", descr="fixture {}".format(func.__name__), where=baseid + scope or "function", + descr="Fixture '{}'".format(func.__name__), + where=baseid, ) self.params = params self.argnames = getfuncargnames(func, is_method=unittest) @@ -913,7 +919,7 @@ class FixtureDef(object): return hook.pytest_fixture_setup(fixturedef=self, request=request) def __repr__(self): - return "" % ( + return "" % ( self.argname, self.scope, 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. """ is_yield_function = is_generator(function) - msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__) - warning = RemovedInPytest4Warning(msg) + warning = FIXTURE_FUNCTION_CALL.format( + name=fixture_marker.name or function.__name__ + ) if is_yield_function: @@ -1168,7 +1175,7 @@ class FixtureManager(object): def pytest_plugin_registered(self, plugin): nodeid = None try: - p = py.path.local(plugin.__file__) + p = py.path.local(plugin.__file__).realpath() except AttributeError: pass else: @@ -1301,9 +1308,7 @@ class FixtureManager(object): filename, lineno = getfslineno(obj) warnings.warn_explicit( - RemovedInPytest4Warning( - deprecated.FUNCARG_PREFIX.format(name=name) - ), + deprecated.FUNCARG_PREFIX.format(name=name), category=None, filename=str(filename), lineno=lineno + 1, diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index cac745b3d..fe6711ac7 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function import logging -from contextlib import closing, contextmanager +from contextlib import contextmanager import re import six @@ -415,7 +415,6 @@ class LoggingPlugin(object): else: self.log_file_handler = None - # initialized during pytest_runtestloop self.log_cli_handler = None def _log_cli_enabled(self): @@ -426,6 +425,22 @@ class LoggingPlugin(object): "--log-cli-level" ) is not None or self._config.getini("log_cli") + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_collection(self): + # This has to be called before the first log message is logged, + # so we can access the terminal reporter plugin. + self._setup_cli_logging() + + with self.live_logs_context(): + if self.log_cli_handler: + self.log_cli_handler.set_when("collection") + + if self.log_file_handler is not None: + with catching_logs(self.log_file_handler, level=self.log_file_level): + yield + else: + yield + @contextmanager def _runtest_for(self, item, when): """Implements the internals of pytest_runtest_xxx() hook.""" @@ -485,22 +500,15 @@ class LoggingPlugin(object): @pytest.hookimpl(hookwrapper=True) def pytest_runtestloop(self, session): """Runs all collected test items.""" - self._setup_cli_logging() - with self.live_logs_context: + with self.live_logs_context(): if self.log_file_handler is not None: - with closing(self.log_file_handler): - with catching_logs( - self.log_file_handler, level=self.log_file_level - ): - yield # run all the tests + with catching_logs(self.log_file_handler, level=self.log_file_level): + yield # run all the tests else: yield # run all the tests def _setup_cli_logging(self): - """Sets up the handler and logger for the Live Logs feature, if enabled. - - This must be done right before starting the loop so we can access the terminal reporter plugin. - """ + """Sets up the handler and logger for the Live Logs feature, if enabled.""" terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") if self._log_cli_enabled() and terminal_reporter is not None: capture_manager = self._config.pluginmanager.get_plugin("capturemanager") @@ -530,11 +538,14 @@ class LoggingPlugin(object): self._config, "log_cli_level", "log_level" ) self.log_cli_handler = log_cli_handler - self.live_logs_context = catching_logs( + self.live_logs_context = lambda: catching_logs( log_cli_handler, formatter=log_cli_formatter, level=log_cli_level ) else: - self.live_logs_context = dummy_context_manager() + self.live_logs_context = lambda: dummy_context_manager() + # Note that the lambda for the live_logs_context is needed because + # live_logs_context can otherwise not be entered multiple times due + # to limitations of contextlib.contextmanager class _LiveLoggingStreamHandler(logging.StreamHandler): diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 5ad91dfc4..8d4176aea 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -156,7 +156,10 @@ def pytest_addoption(parser): dest="basetemp", default=None, metavar="dir", - help="base temporary directory for this test run.", + help=( + "base temporary directory for this test run." + "(warning: this directory is removed if it exists)" + ), ) @@ -182,10 +185,13 @@ def wrap_session(config, doit): session.exitstatus = EXIT_TESTSFAILED except KeyboardInterrupt: excinfo = _pytest._code.ExceptionInfo() - if initstate < 2 and isinstance(excinfo.value, exit.Exception): + exitstatus = EXIT_INTERRUPTED + if initstate <= 2 and isinstance(excinfo.value, exit.Exception): sys.stderr.write("{}: {}\n".format(excinfo.typename, excinfo.value.msg)) + if excinfo.value.returncode is not None: + exitstatus = excinfo.value.returncode config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - session.exitstatus = EXIT_INTERRUPTED + session.exitstatus = exitstatus except: # noqa excinfo = _pytest._code.ExceptionInfo() config.notify_exception(excinfo, config.option) @@ -487,7 +493,7 @@ class Session(nodes.FSCollector): from _pytest.python import Package names = self._parsearg(arg) - argpath = names.pop(0) + argpath = names.pop(0).realpath() paths = [] root = self diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index e3918ca6a..390057428 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -24,11 +24,6 @@ __all__ = [ ] -class MarkerError(Exception): - - """Error in use of a pytest marker/attribute.""" - - def param(*values, **kw): """Specify a parameter in `pytest.mark.parametrize`_ calls or :ref:`parametrized fixtures `. @@ -163,9 +158,9 @@ def pytest_configure(config): empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) - if empty_parameterset not in ("skip", "xfail", None, ""): + if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""): raise UsageError( - "{!s} must be one of skip and xfail," + "{!s} must be one of skip, xfail or fail_at_collect" " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset) ) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 8e8937d59..32822c2bb 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -6,6 +6,7 @@ from operator import attrgetter import attr +from _pytest.outcomes import fail from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE from ..compat import NOTSET, getfslineno, MappingMixin from six.moves import map @@ -32,11 +33,19 @@ def istestfunc(func): def get_empty_parameterset_mark(config, argnames, func): + from ..nodes import Collector + requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) if requested_mark in ("", None, "skip"): mark = MARK_GEN.skip elif requested_mark == "xfail": mark = MARK_GEN.xfail(run=False) + elif requested_mark == "fail_at_collect": + f_name = func.__name__ + _, lineno = getfslineno(func) + raise Collector.CollectError( + "Empty parameter set in '%s' at line %d" % (f_name, lineno) + ) else: raise LookupError(requested_mark) fs, lineno = getfslineno(func) @@ -307,7 +316,7 @@ def _marked(func, mark): return any(mark == info.combined for info in func_mark) -@attr.s +@attr.s(repr=False) class MarkInfo(object): """ Marking object created by :class:`MarkDecorator` instances. """ @@ -385,7 +394,7 @@ class MarkGenerator(object): x = marker.split("(", 1)[0] values.add(x) if name not in self._markers: - raise AttributeError("%r not a registered marker" % (name,)) + fail("{!r} not a registered marker".format(name), pytrace=False) MARK_GEN = MarkGenerator() diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 29d1f0a87..d80895ab5 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -9,6 +9,7 @@ import attr import _pytest import _pytest._code from _pytest.compat import getfslineno +from _pytest.outcomes import fail from _pytest.mark.structures import NodeKeywords, MarkInfo @@ -61,11 +62,11 @@ class _CompatProperty(object): if obj is None: return self - # TODO: reenable in the features branch - # warnings.warn( - # "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format( - # name=self.name, owner=type(owner).__name__), - # PendingDeprecationWarning, stacklevel=2) + from _pytest.deprecated import COMPAT_PROPERTY + + warnings.warn( + COMPAT_PROPERTY.format(name=self.name, owner=owner.__name__), stacklevel=2 + ) return getattr(__import__("pytest"), self.name) @@ -126,11 +127,10 @@ class Node(object): if isinstance(maybe_compatprop, _CompatProperty): return getattr(__import__("pytest"), name) else: + from _pytest.deprecated import CUSTOM_CLASS + cls = getattr(self, name) - # TODO: reenable in the features branch - # warnings.warn("use of node.%s is deprecated, " - # "use pytest_pycollect_makeitem(...) to create custom " - # "collection nodes" % name, category=DeprecationWarning) + self.warn(CUSTOM_CLASS.format(name=name, type_name=type(self).__name__)) return cls def __repr__(self): @@ -347,6 +347,9 @@ class Node(object): pass def _repr_failure_py(self, excinfo, style=None): + if excinfo.errisinstance(fail.Exception): + if not excinfo.value.pytrace: + return six.text_type(excinfo.value) fm = self.session._fixturemanager if excinfo.errisinstance(fm.FixtureLookupError): return excinfo.value.formatrepr() diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index f6093ef76..4c7958384 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -49,18 +49,24 @@ class Failed(OutcomeException): class Exit(KeyboardInterrupt): """ raised for immediate program exits (no tracebacks/summaries)""" - def __init__(self, msg="unknown reason"): + def __init__(self, msg="unknown reason", returncode=None): self.msg = msg + self.returncode = returncode KeyboardInterrupt.__init__(self, msg) # exposed helper methods -def exit(msg): - """ exit testing process as if KeyboardInterrupt was triggered. """ +def exit(msg, returncode=None): + """ + Exit testing process as if KeyboardInterrupt was triggered. + + :param str msg: message to display upon exit. + :param int returncode: return code to be used when exiting pytest. + """ __tracebackhide__ = True - raise Exit(msg) + raise Exit(msg, returncode) exit.Exception = Exit diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py new file mode 100644 index 000000000..cda5e9947 --- /dev/null +++ b/src/_pytest/pathlib.py @@ -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) diff --git a/src/_pytest/paths.py b/src/_pytest/paths.py deleted file mode 100644 index 031ea6b26..000000000 --- a/src/_pytest/paths.py +++ /dev/null @@ -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) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index e3bf060e3..8782a30ba 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -17,13 +17,14 @@ from weakref import WeakKeyDictionary from _pytest.capture import MultiCapture, SysCapture from _pytest._code import Source -import py -import pytest from _pytest.main import Session, EXIT_INTERRUPTED, EXIT_OK from _pytest.assertion.rewrite import AssertionRewritingHook -from _pytest.compat import Path +from _pytest.pathlib import Path from _pytest.compat import safe_str +import py +import pytest + IGNORE_PAM = [ # filenames added when obtaining details about the current user u"/var/lib/sss/mc/passwd" ] @@ -61,6 +62,11 @@ def pytest_configure(config): config.pluginmanager.register(checker) +def raise_on_kwargs(kwargs): + if kwargs: + raise TypeError("Unexpected arguments: {}".format(", ".join(sorted(kwargs)))) + + class LsofFdLeakChecker(object): def get_open_files(self): out = self._exec_lsof() @@ -482,11 +488,16 @@ class Testdir(object): """ + class TimeoutExpired(Exception): + pass + def __init__(self, request, tmpdir_factory): self.request = request self._mod_collections = WeakKeyDictionary() name = request.function.__name__ self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) + self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) + os.environ["PYTEST_DEBUG_TEMPROOT"] = str(self.test_tmproot) self.plugins = [] self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() @@ -513,6 +524,7 @@ class Testdir(object): self._sys_modules_snapshot.restore() self._sys_path_snapshot.restore() self._cwd_snapshot.restore() + os.environ.pop("PYTEST_DEBUG_TEMPROOT", None) def __take_sys_modules_snapshot(self): # some zope modules used by twisted-related tests keep internal state @@ -1039,14 +1051,23 @@ class Testdir(object): return popen - def run(self, *cmdargs): + def run(self, *cmdargs, **kwargs): """Run a command with arguments. Run a process using subprocess.Popen saving the stdout and stderr. + :param args: the sequence of arguments to pass to `subprocess.Popen()` + :param timeout: the period in seconds after which to timeout and raise + :py:class:`Testdir.TimeoutExpired` + Returns a :py:class:`RunResult`. """ + __tracebackhide__ = True + + timeout = kwargs.pop("timeout", None) + raise_on_kwargs(kwargs) + cmdargs = [ str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs ] @@ -1061,7 +1082,40 @@ class Testdir(object): popen = self.popen( cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") ) - 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: f1.close() f2.close() @@ -1108,9 +1162,15 @@ class Testdir(object): with "runpytest-" so they do not conflict with the normal numbered pytest location for temporary files and directories. + :param args: the sequence of arguments to pass to the pytest subprocess + :param timeout: the period in seconds after which to timeout and raise + :py:class:`Testdir.TimeoutExpired` + Returns a :py:class:`RunResult`. """ + __tracebackhide__ = True + p = py.path.local.make_numbered_dir( prefix="runpytest-", keep=None, rootdir=self.tmpdir ) @@ -1119,7 +1179,7 @@ class Testdir(object): if plugins: args = ("-p", plugins[0]) + args args = self._getpytestargs() + args - return self.run(*args) + return self.run(*args, timeout=kwargs.get("timeout")) def spawn_pytest(self, string, expect_timeout=10.0): """Run pytest using pexpect. @@ -1267,6 +1327,7 @@ class LineMatcher(object): matches and non-matches are also printed on stdout. """ + __tracebackhide__ = True self._match_lines(lines2, fnmatch, "fnmatch") def re_match_lines(self, lines2): @@ -1278,6 +1339,7 @@ class LineMatcher(object): The matches and non-matches are also printed on stdout. """ + __tracebackhide__ = True self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match") def _match_lines(self, lines2, match_func, match_nickname): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 0717301ed..ef3e3a730 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -13,11 +13,10 @@ from textwrap import dedent import py import six from _pytest.main import FSHookProxy -from _pytest.mark import MarkerError from _pytest.config import hookimpl import _pytest -import pluggy +from _pytest._code import filter_traceback from _pytest import fixtures from _pytest import nodes from _pytest import deprecated @@ -47,37 +46,6 @@ from _pytest.mark.structures import ( ) from _pytest.warning_types import RemovedInPytest4Warning, PytestWarning -# relative paths that we use to filter traceback entries from appearing to the user; -# see filter_traceback -# note: if we need to add more paths than what we have now we should probably use a list -# for better maintenance -_pluggy_dir = py.path.local(pluggy.__file__.rstrip("oc")) -# pluggy is either a package or a single module depending on the version -if _pluggy_dir.basename == "__init__.py": - _pluggy_dir = _pluggy_dir.dirpath() -_pytest_dir = py.path.local(_pytest.__file__).dirpath() -_py_dir = py.path.local(py.__file__).dirpath() - - -def filter_traceback(entry): - """Return True if a TracebackEntry instance should be removed from tracebacks: - * dynamically generated code (no code to show up for it); - * internal traceback from pytest or its internal libraries, py and pluggy. - """ - # entry.path might sometimes return a str object when the entry - # points to dynamically generated code - # see https://bitbucket.org/pytest-dev/py/issues/71 - raw_filename = entry.frame.code.raw.co_filename - is_generated = "<" in raw_filename and ">" in raw_filename - if is_generated: - return False - # entry.path might point to a non-existing file, in which case it will - # also return a str object. see #1133 - p = py.path.local(entry.path) - return ( - not p.relto(_pluggy_dir) and not p.relto(_pytest_dir) and not p.relto(_py_dir) - ) - def pyobj_property(name): def get(self): @@ -159,8 +127,8 @@ def pytest_generate_tests(metafunc): alt_spellings = ["parameterize", "parametrise", "parameterise"] for attr in alt_spellings: if hasattr(metafunc.function, attr): - msg = "{0} has '{1}', spelling should be 'parametrize'" - raise MarkerError(msg.format(metafunc.function.__name__, attr)) + msg = "{0} has '{1}' mark, spelling should be 'parametrize'" + fail(msg.format(metafunc.function.__name__, attr), pytrace=False) for marker in metafunc.definition.iter_markers(name="parametrize"): metafunc.parametrize(*marker.args, **marker.kwargs) @@ -760,12 +728,6 @@ class FunctionMixin(PyobjMixin): for entry in excinfo.traceback[1:-1]: entry.set_repr_style("short") - def _repr_failure_py(self, excinfo, style="long"): - if excinfo.errisinstance(fail.Exception): - if not excinfo.value.pytrace: - return six.text_type(excinfo.value) - return super(FunctionMixin, self)._repr_failure_py(excinfo, style=style) - def repr_failure(self, excinfo, outerr=None): assert outerr is None, "XXX outerr usage is deprecated" style = self.config.option.tbstyle @@ -799,7 +761,10 @@ class Generator(FunctionMixin, PyCollector): "%r generated tests with non-unique name %r" % (self, name) ) seen[name] = True - values.append(self.Function(name, self, args=args, callobj=call)) + with warnings.catch_warnings(): + # ignore our own deprecation warning + function_class = self.Function + values.append(function_class(name, self, args=args, callobj=call)) self.warn(deprecated.YIELD_TESTS) return values @@ -984,7 +949,9 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition) - scopenum = scope2index(scope, descr="call to {}".format(self.parametrize)) + scopenum = scope2index( + scope, descr="parametrize() call in {}".format(self.function.__name__) + ) # create the new calls: if we are parametrize() multiple times (by applying the decorator # more than once) then we accumulate those calls generating the cartesian product @@ -1023,15 +990,16 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): idfn = ids ids = None if ids: + func_name = self.function.__name__ if len(ids) != len(parameters): - raise ValueError( - "%d tests specified with %d ids" % (len(parameters), len(ids)) - ) + msg = "In {}: {} parameter sets specified, with different number of ids: {}" + fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False) for id_value in ids: if id_value is not None and not isinstance(id_value, six.string_types): - msg = "ids must be list of strings, found: %s (type: %s)" - raise ValueError( - msg % (saferepr(id_value), type(id_value).__name__) + msg = "In {}: ids must be list of strings, found: {} (type: {!r})" + fail( + msg.format(func_name, saferepr(id_value), type(id_value)), + pytrace=False, ) ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) return ids @@ -1056,9 +1024,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): valtypes = dict.fromkeys(argnames, "funcargs") for arg in indirect: if arg not in argnames: - raise ValueError( - "indirect given to %r: fixture %r doesn't exist" - % (self.function, arg) + fail( + "In {}: indirect fixture '{}' doesn't exist".format( + self.function.__name__, arg + ), + pytrace=False, ) valtypes[arg] = "params" return valtypes @@ -1072,19 +1042,25 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): :raise ValueError: if validation fails. """ default_arg_names = set(get_default_arg_names(self.function)) + func_name = self.function.__name__ for arg in argnames: if arg not in self.fixturenames: if arg in default_arg_names: - raise ValueError( - "%r already takes an argument %r with a default value" - % (self.function, arg) + fail( + "In {}: function already takes an argument '{}' with a default value".format( + func_name, arg + ), + pytrace=False, ) else: if isinstance(indirect, (tuple, list)): name = "fixture" if arg in indirect else "argument" else: name = "fixture" if indirect else "argument" - raise ValueError("%r uses no %s %r" % (self.function, name, arg)) + fail( + "In {}: function uses no {} '{}'".format(func_name, name, arg), + pytrace=False, + ) def addcall(self, funcargs=None, id=NOTSET, param=NOTSET): """ Add a new call to the underlying test function during the collection phase of a test run. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index e1b674fa8..62c9158fb 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -43,45 +43,10 @@ def deprecated_call(func=None, *args, **kwargs): in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings types above. """ - if not func: - return _DeprecatedCallContext() - else: - __tracebackhide__ = True - with _DeprecatedCallContext(): - return func(*args, **kwargs) - - -class _DeprecatedCallContext(object): - """Implements the logic to capture deprecation warnings as a context manager.""" - - def __enter__(self): - self._captured_categories = [] - self._old_warn = warnings.warn - self._old_warn_explicit = warnings.warn_explicit - warnings.warn_explicit = self._warn_explicit - warnings.warn = self._warn - - def _warn_explicit(self, message, category, *args, **kwargs): - self._captured_categories.append(category) - - def _warn(self, message, category=None, *args, **kwargs): - if isinstance(message, Warning): - self._captured_categories.append(message.__class__) - else: - self._captured_categories.append(category) - - def __exit__(self, exc_type, exc_val, exc_tb): - warnings.warn_explicit = self._old_warn_explicit - warnings.warn = self._old_warn - - if exc_type is None: - deprecation_categories = (DeprecationWarning, PendingDeprecationWarning) - if not any( - issubclass(c, deprecation_categories) for c in self._captured_categories - ): - __tracebackhide__ = True - msg = "Did not produce DeprecationWarning or PendingDeprecationWarning" - raise AssertionError(msg) + __tracebackhide__ = True + if func is not None: + args = (func,) + args + return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) def warns(expected_warning, *args, **kwargs): @@ -116,6 +81,7 @@ def warns(expected_warning, *args, **kwargs): Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... """ + __tracebackhide__ = True match_expr = None if not args: if "match" in kwargs: @@ -183,12 +149,25 @@ class WarningsRecorder(warnings.catch_warnings): raise RuntimeError("Cannot enter %r twice" % self) self._list = super(WarningsRecorder, self).__enter__() warnings.simplefilter("always") + # python3 keeps track of a "filter version", when the filters are + # updated previously seen warnings can be re-warned. python2 has no + # concept of this so we must reset the warnings registry manually. + # trivial patching of `warnings.warn` seems to be enough somehow? + if six.PY2: + + def warn(*args, **kwargs): + return self._saved_warn(*args, **kwargs) + + warnings.warn, self._saved_warn = warn, warnings.warn return self def __exit__(self, *exc_info): if not self._entered: __tracebackhide__ = True raise RuntimeError("Cannot exit %r without entering first" % self) + # see above where `self._saved_warn` is assigned + if six.PY2: + warnings.warn = self._saved_warn super(WarningsRecorder, self).__exit__(*exc_info) diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index 8a972eed7..9ae90e770 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -31,10 +31,9 @@ def pytest_configure(config): config.pluginmanager.register(config._resultlog) from _pytest.deprecated import RESULT_LOG - from _pytest.warning_types import RemovedInPytest4Warning from _pytest.warnings import _issue_config_warning - _issue_config_warning(RemovedInPytest4Warning(RESULT_LOG), config) + _issue_config_warning(RESULT_LOG, config) def pytest_unconfigure(config): diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index f4dbbe61a..8deb330cc 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -676,7 +676,9 @@ class TerminalReporter(object): if fspath: res = mkrel(nodeid).replace("::()", "") # parens-normalization - if nodeid.split("::")[0] != fspath.replace("\\", nodes.SEP): + if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( + "\\", nodes.SEP + ): res += " <- " + self.startdir.bestrelpath(fspath) else: res = "[location]" diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 260d28422..1963f14c0 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -1,22 +1,86 @@ """ support for providing temporary directories to test functions. """ from __future__ import absolute_import, division, print_function +import os import re - import pytest import py from _pytest.monkeypatch import MonkeyPatch +import attr +import tempfile +import warnings + +from .pathlib import ( + Path, + make_numbered_dir, + make_numbered_dir_with_cleanup, + ensure_reset_dir, + LOCK_TIMEOUT, +) -class TempdirFactory(object): +@attr.s +class TempPathFactory(object): """Factory for temporary directories under the common base temp directory. - The base directory can be configured using the ``--basetemp`` option. + The base directory can be configured using the ``--basetemp`` option.""" + + _given_basetemp = attr.ib() + _trace = attr.ib() + _basetemp = attr.ib(default=None) + + @classmethod + def from_config(cls, config): + """ + :param config: a pytest configuration + """ + return cls( + given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir") + ) + + def mktemp(self, basename, numbered=True): + """makes a temporary directory managed by the factory""" + if not numbered: + p = self.getbasetemp().joinpath(basename) + p.mkdir() + else: + p = make_numbered_dir(root=self.getbasetemp(), prefix=basename) + self._trace("mktemp", p) + return p + + def getbasetemp(self): + """ return base temporary directory. """ + if self._basetemp is None: + if self._given_basetemp is not None: + basetemp = Path(self._given_basetemp) + ensure_reset_dir(basetemp) + else: + from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") + temproot = Path(from_env or tempfile.gettempdir()) + user = get_user() or "unknown" + # use a sub-directory in the temproot to speed-up + # make_numbered_dir() call + rootdir = temproot.joinpath("pytest-of-{}".format(user)) + rootdir.mkdir(exist_ok=True) + basetemp = make_numbered_dir_with_cleanup( + prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT + ) + assert basetemp is not None + self._basetemp = t = basetemp + self._trace("new basetemp", t) + return t + else: + return self._basetemp + + +@attr.s +class TempdirFactory(object): + """ + backward comptibility wrapper that implements + :class:``py.path.local`` for :class:``TempPathFactory`` """ - def __init__(self, config): - self.config = config - self.trace = config.trace.get("tmpdir") + _tmppath_factory = attr.ib() def ensuretemp(self, string, dir=1): """ (deprecated) return temporary directory path with @@ -26,6 +90,9 @@ class TempdirFactory(object): and is guaranteed to be empty. """ # py.log._apiwarn(">1.1", "use tmpdir function argument") + from .deprecated import PYTEST_ENSURETEMP + + warnings.warn(PYTEST_ENSURETEMP, stacklevel=2) return self.getbasetemp().ensure(string, dir=dir) def mktemp(self, basename, numbered=True): @@ -33,46 +100,11 @@ class TempdirFactory(object): If ``numbered``, ensure the directory is unique by adding a number prefix greater than any existing one. """ - basetemp = self.getbasetemp() - if not numbered: - p = basetemp.mkdir(basename) - else: - p = py.path.local.make_numbered_dir( - prefix=basename, keep=0, rootdir=basetemp, lock_timeout=None - ) - self.trace("mktemp", p) - return p + return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve()) def getbasetemp(self): - """ return base temporary directory. """ - try: - return self._basetemp - except AttributeError: - basetemp = self.config.option.basetemp - if basetemp: - basetemp = py.path.local(basetemp) - if basetemp.check(): - basetemp.remove() - basetemp.mkdir() - else: - temproot = py.path.local.get_temproot() - user = get_user() - if user: - # use a sub-directory in the temproot to speed-up - # make_numbered_dir() call - rootdir = temproot.join("pytest-of-%s" % user) - else: - rootdir = temproot - rootdir.ensure(dir=1) - basetemp = py.path.local.make_numbered_dir( - prefix="pytest-", rootdir=rootdir - ) - self._basetemp = t = basetemp.realpath() - self.trace("new basetemp", t) - return t - - def finish(self): - self.trace("finish") + """backward compat wrapper for ``_tmppath_factory.getbasetemp``""" + return py.path.local(self._tmppath_factory.getbasetemp().resolve()) def get_user(): @@ -87,10 +119,6 @@ def get_user(): return None -# backward compatibility -TempdirHandler = TempdirFactory - - def pytest_configure(config): """Create a TempdirFactory and attach it to the config object. @@ -99,19 +127,36 @@ def pytest_configure(config): to the tmpdir_factory session fixture. """ mp = MonkeyPatch() - t = TempdirFactory(config) - config._cleanup.extend([mp.undo, t.finish]) + tmppath_handler = TempPathFactory.from_config(config) + t = TempdirFactory(tmppath_handler) + config._cleanup.append(mp.undo) + mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) mp.setattr(config, "_tmpdirhandler", t, raising=False) mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False) @pytest.fixture(scope="session") def tmpdir_factory(request): - """Return a TempdirFactory instance for the test session. + """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. """ return request.config._tmpdirhandler +@pytest.fixture(scope="session") +def tmp_path_factory(request): + """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. + """ + return request.config._tmp_path_factory + + +def _mk_tmp(request, factory): + name = request.node.name + name = re.sub(r"[\W]", "_", name) + MAXVAL = 30 + name = name[:MAXVAL] + return factory.mktemp(name, numbered=True) + + @pytest.fixture def tmpdir(request, tmpdir_factory): """Return a temporary directory path object @@ -122,10 +167,20 @@ def tmpdir(request, tmpdir_factory): .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html """ - name = request.node.name - name = re.sub(r"[\W]", "_", name) - MAXVAL = 30 - if len(name) > MAXVAL: - name = name[:MAXVAL] - x = tmpdir_factory.mktemp(name, numbered=True) - return x + return _mk_tmp(request, tmpdir_factory) + + +@pytest.fixture +def tmp_path(request, tmp_path_factory): + """Return a temporary directory path object + which is unique to each test function invocation, + created as a sub directory of the base temporary + directory. The returned object is a :class:`pathlib.Path` + object. + + .. note:: + + in python < 3.6 this is a pathlib2.Path + """ + + return _mk_tmp(request, tmp_path_factory) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 8861f6f2b..55e1f037a 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -1,3 +1,6 @@ +import attr + + class PytestWarning(UserWarning): """ Bases: :class:`UserWarning`. @@ -39,4 +42,19 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning): ) +@attr.s +class UnformattedWarning(object): + """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. + + Using this class avoids to keep all the warning types and messages in this module, avoiding misuse. + """ + + category = attr.ib() + template = attr.ib() + + def format(self, **kwargs): + """Returns an instance of the warning category, formatted with given kwargs""" + return self.category(self.template.format(**kwargs)) + + PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example") diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 6c4b921fa..5574eee8e 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -67,27 +67,27 @@ def catch_warnings_for_item(config, ihook, when, item): Each warning captured triggers the ``pytest_warning_captured`` hook. """ - args = config.getoption("pythonwarnings") or [] + cmdline_filters = config.getoption("pythonwarnings") or [] inifilters = config.getini("filterwarnings") with warnings.catch_warnings(record=True) as log: - filters_configured = args or inifilters or sys.warnoptions - for arg in args: - warnings._setoption(arg) + if not sys.warnoptions: + # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908) + warnings.filterwarnings("always", category=DeprecationWarning) + warnings.filterwarnings("always", category=PendingDeprecationWarning) + # filters should have this precedence: mark, cmdline options, ini + # filters should be applied in the inverse order of precedence for arg in inifilters: _setoption(warnings, arg) + for arg in cmdline_filters: + warnings._setoption(arg) + if item is not None: for mark in item.iter_markers(name="filterwarnings"): for arg in mark.args: _setoption(warnings, arg) - filters_configured = True - - if not filters_configured: - # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908) - warnings.filterwarnings("always", category=DeprecationWarning) - warnings.filterwarnings("always", category=PendingDeprecationWarning) yield diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 41cdba067..0a7eb2e03 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -140,9 +140,16 @@ class TestGeneralUsage(object): assert result.ret result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)]) - def test_issue486_better_reporting_on_conftest_load_failure(self, testdir): + def test_better_reporting_on_conftest_load_failure(self, testdir, request): + """Show a user-friendly traceback on conftest import failures (#486, #3332)""" testdir.makepyfile("") - testdir.makeconftest("import qwerty") + testdir.makeconftest( + """ + def foo(): + import qwerty + foo() + """ + ) result = testdir.runpytest("--help") result.stdout.fnmatch_lines( """ @@ -151,10 +158,23 @@ class TestGeneralUsage(object): """ ) result = testdir.runpytest() + dirname = request.node.name + "0" + exc_name = ( + "ModuleNotFoundError" if sys.version_info >= (3, 6) else "ImportError" + ) result.stderr.fnmatch_lines( - """ - *ERROR*could not load*conftest.py* - """ + [ + "ImportError while loading conftest '*{sep}{dirname}{sep}conftest.py'.".format( + dirname=dirname, sep=os.sep + ), + "conftest.py:3: in ", + " foo()", + "conftest.py:2: in foo", + " import qwerty", + "E {}: No module named {q}qwerty{q}".format( + exc_name, q="'" if six.PY3 else "" + ), + ] ) def test_early_skip(self, testdir): @@ -723,16 +743,26 @@ class TestInvocationVariants(object): monkeypatch.syspath_prepend(p) # module picked up in symlink-ed directory: + # It picks up local/lib/foo/bar (symlink) via sys.path. result = testdir.runpytest("--pyargs", "-v", "foo.bar") testdir.chdir() assert result.ret == 0 - result.stdout.fnmatch_lines( - [ - "*lib/foo/bar/test_bar.py::test_bar PASSED*", - "*lib/foo/bar/test_bar.py::test_other PASSED*", - "*2 passed*", - ] - ) + if hasattr(py.path.local, "mksymlinkto"): + result.stdout.fnmatch_lines( + [ + "lib/foo/bar/test_bar.py::test_bar PASSED*", + "lib/foo/bar/test_bar.py::test_other PASSED*", + "*2 passed*", + ] + ) + else: + result.stdout.fnmatch_lines( + [ + "*lib/foo/bar/test_bar.py::test_bar PASSED*", + "*lib/foo/bar/test_bar.py::test_other PASSED*", + "*2 passed*", + ] + ) def test_cmdline_python_package_not_exists(self, testdir): result = testdir.runpytest("--pyargs", "tpkgwhatv") diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 72b1a78ab..92460cd29 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1184,20 +1184,28 @@ raise ValueError() assert tw.lines[47] == ":15: AttributeError" @pytest.mark.skipif("sys.version_info[0] < 3") - def test_exc_repr_with_raise_from_none_chain_suppression(self, importasmod): + @pytest.mark.parametrize("mode", ["from_none", "explicit_suppress"]) + def test_exc_repr_chain_suppression(self, importasmod, mode): + """Check that exc repr does not show chained exceptions in Python 3. + - When the exception is raised with "from None" + - Explicitly suppressed with "chain=False" to ExceptionInfo.getrepr(). + """ + raise_suffix = " from None" if mode == "from_none" else "" mod = importasmod( """ def f(): try: g() except Exception: - raise AttributeError() from None + raise AttributeError(){raise_suffix} def g(): raise ValueError() - """ + """.format( + raise_suffix=raise_suffix + ) ) excinfo = pytest.raises(AttributeError, mod.f) - r = excinfo.getrepr(style="long") + r = excinfo.getrepr(style="long", chain=mode != "explicit_suppress") tw = TWMock() r.toterminal(tw) for line in tw.lines: @@ -1207,7 +1215,9 @@ raise ValueError() assert tw.lines[2] == " try:" assert tw.lines[3] == " g()" assert tw.lines[4] == " except Exception:" - assert tw.lines[5] == "> raise AttributeError() from None" + assert tw.lines[5] == "> raise AttributeError(){}".format( + raise_suffix + ) assert tw.lines[6] == "E AttributeError" assert tw.lines[7] == "" line = tw.get_write_msg(8) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 64b4a0124..649ebcd38 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -30,6 +30,74 @@ def test_yield_tests_deprecation(testdir): assert result.stdout.str().count("yield tests are deprecated") == 2 +def test_compat_properties_deprecation(testdir): + testdir.makepyfile( + """ + def test_foo(request): + print(request.node.Module) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "*test_compat_properties_deprecation.py:2:*usage of Function.Module is deprecated, " + "please use pytest.Module instead*", + "*1 passed, 1 warnings in*", + ] + ) + + +def test_cached_setup_deprecation(testdir): + testdir.makepyfile( + """ + import pytest + @pytest.fixture + def fix(request): + return request.cached_setup(lambda: 1) + + def test_foo(fix): + assert fix == 1 + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "*test_cached_setup_deprecation.py:4:*cached_setup is deprecated*", + "*1 passed, 1 warnings in*", + ] + ) + + +def test_custom_class_deprecation(testdir): + testdir.makeconftest( + """ + import pytest + + class MyModule(pytest.Module): + + class Class(pytest.Class): + pass + + def pytest_pycollect_makemodule(path, parent): + return MyModule(path, parent) + """ + ) + testdir.makepyfile( + """ + class Test: + def test_foo(self): + pass + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + '*test_custom_class_deprecation.py:1:*"Class" objects in collectors of type "MyModule*', + "*1 passed, 1 warnings in*", + ] + ) + + @pytest.mark.filterwarnings("default") def test_funcarg_prefix_deprecation(testdir): testdir.makepyfile( diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 363982cf9..498b4c5bd 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -908,3 +908,61 @@ def test_live_logging_suspends_capture(has_capture_manager, request): else: assert MockCaptureManager.calls == [] assert out_file.getvalue() == "\nsome message\n" + + +def test_collection_live_logging(testdir): + testdir.makepyfile( + """ + import logging + + logging.getLogger().info("Normal message") + """ + ) + + result = testdir.runpytest("--log-cli-level=INFO") + result.stdout.fnmatch_lines( + [ + "collecting*", + "*--- live log collection ---*", + "*Normal message*", + "collected 0 items", + ] + ) + + +def test_collection_logging_to_file(testdir): + log_file = testdir.tmpdir.join("pytest.log").strpath + + testdir.makeini( + """ + [pytest] + log_file={} + log_file_level = INFO + """.format( + log_file + ) + ) + + testdir.makepyfile( + """ + import logging + + logging.getLogger().info("Normal message") + + def test_simple(): + logging.getLogger().debug("debug message in test_simple") + logging.getLogger().info("info message in test_simple") + """ + ) + + result = testdir.runpytest() + + assert "--- live log collection ---" not in result.stdout.str() + + assert result.ret == 0 + assert os.path.isfile(log_file) + with open(log_file, encoding="utf-8") as rfh: + contents = rfh.read() + assert "Normal message" in contents + assert "debug message in test_simple" not in contents + assert "info message in test_simple" in contents diff --git a/testing/python/collect.py b/testing/python/collect.py index cbfc4a9d2..61039a506 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -240,6 +240,9 @@ class TestClass(object): assert result.ret == EXIT_NOTESTSCOLLECTED +@pytest.mark.filterwarnings( + "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead" +) class TestGenerator(object): def test_generative_functions(self, testdir): modcol = testdir.getmodulecol( @@ -1255,6 +1258,9 @@ class TestReportInfo(object): assert lineno == 1 assert msg == "TestClass" + @pytest.mark.filterwarnings( + "ignore:usage of Generator.Function is deprecated, please use pytest.Function instead" + ) def test_generator_reportinfo(self, testdir): modcol = testdir.getmodulecol( """ diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 7ec7e9c1c..f21f7a861 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -6,7 +6,7 @@ import pytest from _pytest.pytester import get_public_names from _pytest.fixtures import FixtureLookupError, FixtureRequest from _pytest import fixtures -from _pytest.compat import Path +from _pytest.pathlib import Path def test_getfuncargnames(): @@ -993,6 +993,7 @@ class TestRequestCachedSetup(object): ) reprec.assertoutcome(passed=4) + @pytest.mark.filterwarnings("ignore:cached_setup is deprecated") def test_request_cachedsetup_extrakey(self, testdir): item1 = testdir.getitem("def test_func(): pass") req1 = fixtures.FixtureRequest(item1) @@ -1010,6 +1011,7 @@ class TestRequestCachedSetup(object): assert ret1 == ret1b assert ret2 == ret2b + @pytest.mark.filterwarnings("ignore:cached_setup is deprecated") def test_request_cachedsetup_cache_deletion(self, testdir): item1 = testdir.getitem("def test_func(): pass") req1 = fixtures.FixtureRequest(item1) @@ -1221,8 +1223,7 @@ class TestFixtureUsages(object): result = testdir.runpytest_inprocess() result.stdout.fnmatch_lines( ( - "*ValueError: fixture badscope from test_invalid_scope.py has an unsupported" - " scope value 'functions'" + "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'" ) ) @@ -3611,16 +3612,15 @@ class TestParameterizedSubRequest(object): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - """ - E*Failed: The requested fixture has no parameter defined for test: - E* test_call_from_fixture.py::test_foo - E* - E*Requested fixture 'fix_with_param' defined in: - E*test_call_from_fixture.py:4 - E*Requested here: - E*test_call_from_fixture.py:9 - *1 error* - """ + [ + "The requested fixture has no parameter defined for test:", + " test_call_from_fixture.py::test_foo", + "Requested fixture 'fix_with_param' defined in:", + "test_call_from_fixture.py:4", + "Requested here:", + "test_call_from_fixture.py:9", + "*1 error in*", + ] ) def test_call_from_test(self, testdir): @@ -3638,16 +3638,15 @@ class TestParameterizedSubRequest(object): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - """ - E*Failed: The requested fixture has no parameter defined for test: - E* test_call_from_test.py::test_foo - E* - E*Requested fixture 'fix_with_param' defined in: - E*test_call_from_test.py:4 - E*Requested here: - E*test_call_from_test.py:8 - *1 failed* - """ + [ + "The requested fixture has no parameter defined for test:", + " test_call_from_test.py::test_foo", + "Requested fixture 'fix_with_param' defined in:", + "test_call_from_test.py:4", + "Requested here:", + "test_call_from_test.py:8", + "*1 failed*", + ] ) def test_external_fixture(self, testdir): @@ -3669,16 +3668,16 @@ class TestParameterizedSubRequest(object): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - """ - E*Failed: The requested fixture has no parameter defined for test: - E* test_external_fixture.py::test_foo - E* - E*Requested fixture 'fix_with_param' defined in: - E*conftest.py:4 - E*Requested here: - E*test_external_fixture.py:2 - *1 failed* - """ + [ + "The requested fixture has no parameter defined for test:", + " test_external_fixture.py::test_foo", + "", + "Requested fixture 'fix_with_param' defined in:", + "conftest.py:4", + "Requested here:", + "test_external_fixture.py:2", + "*1 failed*", + ] ) def test_non_relative_path(self, testdir): @@ -3713,16 +3712,16 @@ class TestParameterizedSubRequest(object): testdir.syspathinsert(fixdir) result = testdir.runpytest() result.stdout.fnmatch_lines( - """ - E*Failed: The requested fixture has no parameter defined for test: - E* test_foos.py::test_foo - E* - E*Requested fixture 'fix_with_param' defined in: - E*fix.py:4 - E*Requested here: - E*test_foos.py:4 - *1 failed* - """ + [ + "The requested fixture has no parameter defined for test:", + " test_foos.py::test_foo", + "", + "Requested fixture 'fix_with_param' defined in:", + "*fix.py:4", + "Requested here:", + "test_foos.py:4", + "*1 failed*", + ] ) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 2172c5e0c..fea59ee98 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -127,10 +127,11 @@ class TestMetafunc(object): pass metafunc = self.Metafunc(func) - try: + with pytest.raises( + pytest.fail.Exception, + match=r"parametrize\(\) call in func got an unexpected scope value 'doggy'", + ): metafunc.parametrize("x", [1], scope="doggy") - except ValueError as ve: - assert "has an unsupported scope value 'doggy'" in str(ve) def test_find_parametrized_scope(self): """unittest for _find_parametrized_scope (#3941)""" @@ -206,16 +207,13 @@ class TestMetafunc(object): metafunc = self.Metafunc(func) - pytest.raises( - ValueError, lambda: metafunc.parametrize("x", [1, 2], ids=["basic"]) - ) + with pytest.raises(pytest.fail.Exception): + metafunc.parametrize("x", [1, 2], ids=["basic"]) - pytest.raises( - ValueError, - lambda: metafunc.parametrize( + with pytest.raises(pytest.fail.Exception): + metafunc.parametrize( ("x", "y"), [("abc", "def"), ("ghi", "jkl")], ids=["one"] - ), - ) + ) @pytest.mark.issue510 def test_parametrize_empty_list(self): @@ -573,7 +571,7 @@ class TestMetafunc(object): pass metafunc = self.Metafunc(func) - with pytest.raises(ValueError): + with pytest.raises(pytest.fail.Exception): metafunc.parametrize("x, y", [("a", "b")], indirect=["x", "z"]) @pytest.mark.issue714 @@ -1189,7 +1187,9 @@ class TestMetafuncFunctional(object): ) result = testdir.runpytest() result.stdout.fnmatch_lines( - ["*ids must be list of strings, found: 2 (type: int)*"] + [ + "*In test_ids_numbers: ids must be list of strings, found: 2 (type: *'int'>)*" + ] ) def test_parametrize_with_identical_ids_get_unique_names(self, testdir): @@ -1326,13 +1326,13 @@ class TestMetafuncFunctional(object): attr ) ) - reprec = testdir.inline_run("--collectonly") - failures = reprec.getfailures() - assert len(failures) == 1 - expectederror = "MarkerError: test_foo has '{}', spelling should be 'parametrize'".format( - attr + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines( + [ + "test_foo has '{}' mark, spelling should be 'parametrize'".format(attr), + "*1 error in*", + ] ) - assert expectederror in failures[0].longrepr.reprcrash.message class TestMetafuncFunctionalAuto(object): diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 07da5d5ee..a2df0ae37 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -4,7 +4,7 @@ import textwrap import py import pytest from _pytest.config import PytestPluginManager -from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR +from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_OK, EXIT_USAGEERROR @pytest.fixture(scope="module", params=["global", "inpackage"]) @@ -186,6 +186,52 @@ def test_conftest_confcutdir(testdir): assert "warning: could not load initial" not in result.stdout.str() +@pytest.mark.skipif( + not hasattr(py.path.local, "mksymlinkto"), + reason="symlink not available on this platform", +) +def test_conftest_symlink(testdir): + """Ensure that conftest.py is used for resolved symlinks.""" + realtests = testdir.tmpdir.mkdir("real").mkdir("app").mkdir("tests") + testdir.tmpdir.join("symlinktests").mksymlinkto(realtests) + testdir.makepyfile( + **{ + "real/app/tests/test_foo.py": "def test1(fixture): pass", + "real/conftest.py": textwrap.dedent( + """ + import pytest + + print("conftest_loaded") + + @pytest.fixture + def fixture(): + print("fixture_used") + """ + ), + } + ) + result = testdir.runpytest("-vs", "symlinktests") + result.stdout.fnmatch_lines( + [ + "*conftest_loaded*", + "real/app/tests/test_foo.py::test1 fixture_used", + "PASSED", + ] + ) + assert result.ret == EXIT_OK + + realtests.ensure("__init__.py") + result = testdir.runpytest("-vs", "symlinktests/test_foo.py::test1") + result.stdout.fnmatch_lines( + [ + "*conftest_loaded*", + "real/app/tests/test_foo.py::test1 fixture_used", + "PASSED", + ] + ) + assert result.ret == EXIT_OK + + def test_no_conftest(testdir): testdir.makeconftest("assert 0") result = testdir.runpytest("--noconftest") diff --git a/testing/test_mark.py b/testing/test_mark.py index cc77d2682..48a680297 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -13,7 +13,7 @@ from _pytest.mark import ( transfer_markers, EMPTY_PARAMETERSET_OPTION, ) -from _pytest.nodes import Node +from _pytest.nodes import Node, Collector ignore_markinfo = pytest.mark.filterwarnings( "ignore:MarkInfo objects:pytest.RemovedInPytest4Warning" @@ -247,7 +247,7 @@ def test_marker_without_description(testdir): ) ftdir = testdir.mkdir("ft1_dummy") testdir.tmpdir.join("conftest.py").move(ftdir.join("conftest.py")) - rec = testdir.runpytest_subprocess("--strict") + rec = testdir.runpytest("--strict") rec.assert_outcomes() @@ -302,7 +302,7 @@ def test_strict_prohibits_unregistered_markers(testdir): ) result = testdir.runpytest("--strict") assert result.ret != 0 - result.stdout.fnmatch_lines(["*unregisteredmark*not*registered*"]) + result.stdout.fnmatch_lines(["'unregisteredmark' not a registered marker"]) @pytest.mark.parametrize( @@ -1103,7 +1103,14 @@ class TestMarkDecorator(object): @pytest.mark.parametrize("mark", [None, "", "skip", "xfail"]) def test_parameterset_for_parametrize_marks(testdir, mark): if mark is not None: - testdir.makeini("[pytest]\n{}={}".format(EMPTY_PARAMETERSET_OPTION, mark)) + testdir.makeini( + """ + [pytest] + {}={} + """.format( + EMPTY_PARAMETERSET_OPTION, mark + ) + ) config = testdir.parseconfig() from _pytest.mark import pytest_configure, get_empty_parameterset_mark @@ -1119,6 +1126,34 @@ def test_parameterset_for_parametrize_marks(testdir, mark): assert result_mark.kwargs.get("run") is False +def test_parameterset_for_fail_at_collect(testdir): + testdir.makeini( + """ + [pytest] + {}=fail_at_collect + """.format( + EMPTY_PARAMETERSET_OPTION + ) + ) + + config = testdir.parseconfig() + from _pytest.mark import pytest_configure, get_empty_parameterset_mark + from _pytest.compat import getfslineno + + pytest_configure(config) + + test_func = all + func_name = test_func.__name__ + _, func_lineno = getfslineno(test_func) + expected_errmsg = r"Empty parameter set in '%s' at line %d" % ( + func_name, + func_lineno, + ) + + with pytest.raises(Collector.CollectError, match=expected_errmsg): + get_empty_parameterset_mark(config, ["a"], test_func) + + def test_parameterset_for_parametrize_bad_markname(testdir): with pytest.raises(pytest.UsageError): test_parameterset_for_parametrize_marks(testdir, "bad") diff --git a/testing/test_paths.py b/testing/test_paths.py index 2bb1335fb..2eb07bbd4 100644 --- a/testing/test_paths.py +++ b/testing/test_paths.py @@ -4,7 +4,7 @@ import py import pytest -from _pytest.paths import fnmatch_ex +from _pytest.pathlib import fnmatch_ex class TestPort: diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 5be2bada8..cf6a51367 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -4,6 +4,7 @@ import os import py.path import pytest import sys +import time import _pytest.pytester as pytester from _pytest.pytester import HookRecorder from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot @@ -401,3 +402,34 @@ def test_testdir_subprocess(testdir): def test_unicode_args(testdir): result = testdir.runpytest("-k", u"💩") assert result.ret == EXIT_NOTESTSCOLLECTED + + +def test_testdir_run_no_timeout(testdir): + testfile = testdir.makepyfile("def test_no_timeout(): pass") + assert testdir.runpytest_subprocess(testfile).ret == EXIT_OK + + +def test_testdir_run_with_timeout(testdir): + testfile = testdir.makepyfile("def test_no_timeout(): pass") + + timeout = 120 + + start = time.time() + result = testdir.runpytest_subprocess(testfile, timeout=timeout) + end = time.time() + duration = end - start + + assert result.ret == EXIT_OK + assert duration < timeout + + +def test_testdir_run_timeout_expires(testdir): + testfile = testdir.makepyfile( + """ + import time + + def test_timeout(): + time.sleep(10)""" + ) + with pytest.raises(testdir.TimeoutExpired): + testdir.runpytest_subprocess(testfile, timeout=1) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 82bd66c55..3ae543248 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -76,9 +76,8 @@ class TestDeprecatedCall(object): ) def test_deprecated_call_raises(self): - with pytest.raises(AssertionError) as excinfo: + with pytest.raises(pytest.fail.Exception, match="No warnings of type"): pytest.deprecated_call(self.dep, 3, 5) - assert "Did not produce" in str(excinfo) def test_deprecated_call(self): pytest.deprecated_call(self.dep, 0, 5) @@ -100,7 +99,7 @@ class TestDeprecatedCall(object): assert warn_explicit is warnings.warn_explicit def test_deprecated_explicit_call_raises(self): - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): pytest.deprecated_call(self.dep_explicit, 3) def test_deprecated_explicit_call(self): @@ -116,8 +115,8 @@ class TestDeprecatedCall(object): def f(): pass - msg = "Did not produce DeprecationWarning or PendingDeprecationWarning" - with pytest.raises(AssertionError, match=msg): + msg = "No warnings of type (.*DeprecationWarning.*, .*PendingDeprecationWarning.*)" + with pytest.raises(pytest.fail.Exception, match=msg): if mode == "call": pytest.deprecated_call(f) else: @@ -179,12 +178,20 @@ class TestDeprecatedCall(object): def f(): warnings.warn(warning("hi")) - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): pytest.deprecated_call(f) - with pytest.raises(AssertionError): + with pytest.raises(pytest.fail.Exception): with pytest.deprecated_call(): f() + def test_deprecated_call_supports_match(self): + with pytest.deprecated_call(match=r"must be \d+$"): + warnings.warn("value must be 42", DeprecationWarning) + + with pytest.raises(pytest.fail.Exception): + with pytest.deprecated_call(match=r"must be \d+$"): + warnings.warn("this is not here", DeprecationWarning) + class TestWarns(object): def test_strings(self): @@ -343,3 +350,13 @@ class TestWarns(object): with pytest.warns(UserWarning, match=r"aaa"): warnings.warn("bbbbbbbbbb", UserWarning) warnings.warn("cccccccccc", UserWarning) + + @pytest.mark.filterwarnings("ignore") + def test_can_capture_previously_warned(self): + def f(): + warnings.warn(UserWarning("ohai")) + return 10 + + assert f() == 10 + assert pytest.warns(UserWarning, f) == 10 + assert pytest.warns(UserWarning, f) == 10 diff --git a/testing/test_runner.py b/testing/test_runner.py index 741180692..a3fffe81a 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -570,7 +570,20 @@ def test_pytest_exit_msg(testdir): result.stderr.fnmatch_lines(["Exit: oh noes"]) -def test_pytest_fail_notrace(testdir): +def test_pytest_exit_returncode(testdir): + testdir.makepyfile( + """ + import pytest + def test_foo(): + pytest.exit("some exit msg", 99) + """ + ) + result = testdir.runpytest() + assert result.ret == 99 + + +def test_pytest_fail_notrace_runtest(testdir): + """Test pytest.fail(..., pytrace=False) does not show tracebacks during test run.""" testdir.makepyfile( """ import pytest @@ -585,6 +598,21 @@ def test_pytest_fail_notrace(testdir): assert "def teardown_function" not in result.stdout.str() +def test_pytest_fail_notrace_collection(testdir): + """Test pytest.fail(..., pytrace=False) does not show tracebacks during collection.""" + testdir.makepyfile( + """ + import pytest + def some_internal_function(): + pytest.fail("hello", pytrace=False) + some_internal_function() + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["hello"]) + assert "def some_internal_function()" not in result.stdout.str() + + @pytest.mark.parametrize("str_prefix", ["u", ""]) def test_pytest_fail_notrace_non_ascii(testdir, str_prefix): """Fix pytest.fail with pytrace=False with non-ascii characters (#1178). diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 7651f3ab3..af2dc2f00 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -154,7 +154,7 @@ class TestTerminal(object): ) result = testdir.runpytest(p2) result.stdout.fnmatch_lines(["*test_p2.py .*", "*1 passed*"]) - result = testdir.runpytest("-v", p2) + result = testdir.runpytest("-vv", p2) result.stdout.fnmatch_lines( ["*test_p2.py::TestMore::test_p1* <- *test_p1.py*PASSED*"] ) @@ -170,7 +170,7 @@ class TestTerminal(object): """ ) ) - result = testdir.runpytest("-v") + result = testdir.runpytest("-vv") assert result.ret == 0 result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"]) assert " <- " not in result.stdout.str() diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 487f9b21e..9f4158eb7 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -1,7 +1,10 @@ from __future__ import absolute_import, division, print_function import sys -import py + +import six + import pytest +from _pytest.pathlib import Path def test_tmpdir_fixture(testdir): @@ -19,11 +22,11 @@ def test_ensuretemp(recwarn): class TestTempdirHandler(object): def test_mktemp(self, testdir): - from _pytest.tmpdir import TempdirFactory + from _pytest.tmpdir import TempdirFactory, TempPathFactory config = testdir.parseconfig() config.option.basetemp = testdir.mkdir("hello") - t = TempdirFactory(config) + t = TempdirFactory(TempPathFactory.from_config(config)) tmp = t.mktemp("world") assert tmp.relto(t.getbasetemp()) == "world0" tmp = t.mktemp("this") @@ -65,10 +68,6 @@ def test_basetemp(testdir): assert mytemp.join("hello").check() -@pytest.mark.skipif( - not hasattr(py.path.local, "mksymlinkto"), - reason="symlink not available on this platform", -) def test_tmpdir_always_is_realpath(testdir): # the reason why tmpdir should be a realpath is that # when you cd to it and do "os.getcwd()" you will anyway @@ -78,7 +77,7 @@ def test_tmpdir_always_is_realpath(testdir): # os.environ["PWD"] realtemp = testdir.tmpdir.mkdir("myrealtemp") linktemp = testdir.tmpdir.join("symlinktemp") - linktemp.mksymlinkto(realtemp) + attempt_symlink_to(linktemp, str(realtemp)) p = testdir.makepyfile( """ def test_1(tmpdir): @@ -111,7 +110,7 @@ def test_tmpdir_factory(testdir): def session_dir(tmpdir_factory): return tmpdir_factory.mktemp('data', numbered=False) def test_some(session_dir): - session_dir.isdir() + assert session_dir.isdir() """ ) reprec = testdir.inline_run() @@ -184,3 +183,113 @@ def test_get_user(monkeypatch): monkeypatch.delenv("USER", raising=False) monkeypatch.delenv("USERNAME", raising=False) assert get_user() is None + + +class TestNumberedDir(object): + PREFIX = "fun-" + + def test_make(self, tmp_path): + from _pytest.pathlib import make_numbered_dir + + for i in range(10): + d = make_numbered_dir(root=tmp_path, prefix=self.PREFIX) + assert d.name.startswith(self.PREFIX) + assert d.name.endswith(str(i)) + + def test_cleanup_lock_create(self, tmp_path): + d = tmp_path.joinpath("test") + d.mkdir() + from _pytest.pathlib import create_cleanup_lock + + lockfile = create_cleanup_lock(d) + with pytest.raises(EnvironmentError, match="cannot create lockfile in .*"): + create_cleanup_lock(d) + + lockfile.unlink() + + def test_lock_register_cleanup_removal(self, tmp_path): + from _pytest.pathlib import create_cleanup_lock, register_cleanup_lock_removal + + lock = create_cleanup_lock(tmp_path) + + registry = [] + register_cleanup_lock_removal(lock, register=registry.append) + + cleanup_func, = registry + + assert lock.is_file() + + cleanup_func(original_pid="intentionally_different") + + assert lock.is_file() + + cleanup_func() + + assert not lock.exists() + + cleanup_func() + + assert not lock.exists() + + def _do_cleanup(self, tmp_path): + self.test_make(tmp_path) + from _pytest.pathlib import cleanup_numbered_dir + + cleanup_numbered_dir( + root=tmp_path, + prefix=self.PREFIX, + keep=2, + consider_lock_dead_if_created_before=0, + ) + + def test_cleanup_keep(self, tmp_path): + self._do_cleanup(tmp_path) + a, b = tmp_path.iterdir() + print(a, b) + + def test_cleanup_locked(self, tmp_path): + + from _pytest import pathlib + + p = pathlib.make_numbered_dir(root=tmp_path, prefix=self.PREFIX) + + pathlib.create_cleanup_lock(p) + + assert not pathlib.ensure_deletable( + p, consider_lock_dead_if_created_before=p.stat().st_mtime - 1 + ) + assert pathlib.ensure_deletable( + p, consider_lock_dead_if_created_before=p.stat().st_mtime + 1 + ) + + def test_rmtree(self, tmp_path): + from _pytest.pathlib import rmtree + + adir = tmp_path / "adir" + adir.mkdir() + rmtree(adir) + + assert not adir.exists() + + adir.mkdir() + afile = adir / "afile" + afile.write_bytes(b"aa") + + rmtree(adir, force=True) + assert not adir.exists() + + def test_cleanup_symlink(self, tmp_path): + the_symlink = tmp_path / (self.PREFIX + "current") + attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5")) + self._do_cleanup(tmp_path) + + +def attempt_symlink_to(path, to_path): + """Try to make a symlink from "path" to "to_path", skipping in case this platform + does not support it or we don't have sufficient privileges (common on Windows).""" + if sys.platform.startswith("win") and six.PY2: + pytest.skip("pathlib for some reason cannot make symlinks on Python 2") + try: + Path(path).symlink_to(Path(to_path)) + except OSError: + pytest.skip("could not create symbolic link") diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 3f748d666..7825f2167 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -430,6 +430,50 @@ def test_hide_pytest_internal_warnings(testdir, ignore_pytest_warnings): ) +@pytest.mark.parametrize("ignore_on_cmdline", [True, False]) +def test_option_precedence_cmdline_over_ini(testdir, ignore_on_cmdline): + """filters defined in the command-line should take precedence over filters in ini files (#3946).""" + testdir.makeini( + """ + [pytest] + filterwarnings = error + """ + ) + testdir.makepyfile( + """ + import warnings + def test(): + warnings.warn(UserWarning('hello')) + """ + ) + args = ["-W", "ignore"] if ignore_on_cmdline else [] + result = testdir.runpytest(*args) + if ignore_on_cmdline: + result.stdout.fnmatch_lines(["* 1 passed in*"]) + else: + result.stdout.fnmatch_lines(["* 1 failed in*"]) + + +def test_option_precedence_mark(testdir): + """Filters defined by marks should always take precedence (#3946).""" + testdir.makeini( + """ + [pytest] + filterwarnings = ignore + """ + ) + testdir.makepyfile( + """ + import pytest, warnings + @pytest.mark.filterwarnings('error') + def test(): + warnings.warn(UserWarning('hello')) + """ + ) + result = testdir.runpytest("-W", "ignore") + result.stdout.fnmatch_lines(["* 1 failed in*"]) + + class TestDeprecationWarningsByDefault: """ Note: all pytest runs are executed in a subprocess so we don't inherit warning filters @@ -451,8 +495,18 @@ class TestDeprecationWarningsByDefault: ) ) - def test_shown_by_default(self, testdir): + @pytest.mark.parametrize("customize_filters", [True, False]) + def test_shown_by_default(self, testdir, customize_filters): + """Show deprecation warnings by default, even if user has customized the warnings filters (#4013).""" self.create_file(testdir) + if customize_filters: + testdir.makeini( + """ + [pytest] + filterwarnings = + once::UserWarning + """ + ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines( [ @@ -468,7 +522,9 @@ class TestDeprecationWarningsByDefault: testdir.makeini( """ [pytest] - filterwarnings = once::UserWarning + filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning """ ) result = testdir.runpytest_subprocess() @@ -479,7 +535,8 @@ class TestDeprecationWarningsByDefault: be displayed normally. """ self.create_file( - testdir, mark='@pytest.mark.filterwarnings("once::UserWarning")' + testdir, + mark='@pytest.mark.filterwarnings("ignore::PendingDeprecationWarning")', ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines( @@ -492,7 +549,12 @@ class TestDeprecationWarningsByDefault: def test_hidden_by_cmdline(self, testdir): self.create_file(testdir) - result = testdir.runpytest_subprocess("-W", "once::UserWarning") + result = testdir.runpytest_subprocess( + "-W", + "ignore::DeprecationWarning", + "-W", + "ignore::PendingDeprecationWarning", + ) assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() def test_hidden_by_system(self, testdir, monkeypatch): diff --git a/tox.ini b/tox.ini index 98d2ce041..86b3b9458 100644 --- a/tox.ini +++ b/tox.ini @@ -194,13 +194,14 @@ commands = python scripts/release.py {posargs} [pytest] minversion = 2.0 -plugins = pytester -addopts = -ra -p pytester --ignore=testing/cx_freeze -rsyncdirs = tox.ini pytest.py _pytest testing +addopts = -ra -p pytester +rsyncdirs = tox.ini doc src testing python_files = test_*.py *_test.py testing/*/*.py python_classes = Test Acceptance python_functions = test -norecursedirs = .tox ja .hg cx_freeze_source testing/example_scripts +# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". +testpaths = testing +norecursedirs = testing/example_scripts xfail_strict=true filterwarnings = error