diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 59128a4fb..b82693c18 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,13 +5,11 @@ repos:
hooks:
- id: black
args: [--safe, --quiet]
- language_version: python3
- repo: https://github.com/asottile/blacken-docs
rev: v1.0.0
hooks:
- id: blacken-docs
additional_dependencies: [black==19.3b0]
- language_version: python3
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.2.3
hooks:
diff --git a/.travis.yml b/.travis.yml
index 8053eed65..af33d672e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,6 +13,10 @@ env:
global:
- PYTEST_ADDOPTS=-vv
+# setuptools-scm needs all tags in order to obtain a proper version
+git:
+ depth: false
+
install:
- python -m pip install --upgrade --pre tox
diff --git a/AUTHORS b/AUTHORS
index 140f4de3d..88bbfe352 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -239,6 +239,7 @@ Tareq Alayan
Ted Xiao
Thomas Grainger
Thomas Hisch
+Tim Hoffmann
Tim Strazny
Tom Dalton
Tom Viner
@@ -258,6 +259,7 @@ Wil Cooley
William Lee
Wim Glenn
Wouter van Ackooy
+Xixi Zhao
Xuan Luong
Xuecong Liao
Zac Hatfield-Dodds
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c4ab6f1bd..811f7475d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,6 @@
-=================
-Changelog history
-=================
+=========
+Changelog
+=========
Versions follow `Semantic Versioning `_ (``..``).
@@ -90,6 +90,24 @@ Removals
- `#5412 `_: ``ExceptionInfo`` objects (returned by ``pytest.raises``) now have the same ``str`` representation as ``repr``, which
avoids some confusion when users use ``print(e)`` to inspect the object.
+ This means code like:
+
+ .. code-block:: python
+
+ with pytest.raises(SomeException) as e:
+ ...
+ assert "some message" in str(e)
+
+
+ Needs to be changed to:
+
+ .. code-block:: python
+
+ with pytest.raises(SomeException) as e:
+ ...
+ assert "some message" in str(e.value)
+
+
Deprecations
@@ -2173,10 +2191,10 @@ Features
design. This introduces new ``Node.iter_markers(name)`` and
``Node.get_closest_marker(name)`` APIs. Users are **strongly encouraged** to
read the `reasons for the revamp in the docs
- `_,
+ `_,
or jump over to details about `updating existing code to use the new APIs
- `_. (`#3317
- `_)
+ `_.
+ (`#3317 `_)
- Now when ``@pytest.fixture`` is applied more than once to the same function a
``ValueError`` is raised. This buggy behavior would cause surprising problems
@@ -2582,10 +2600,10 @@ Features
`_)
- New `pytest_runtest_logfinish
- `_
+ `_
hook which is called when a test item has finished executing, analogous to
`pytest_runtest_logstart
- `_.
+ `_.
(`#3101 `_)
- Improve performance when collecting tests using many fixtures. (`#3107
@@ -3575,7 +3593,7 @@ Bug Fixes
Thanks `@sirex`_ for the report and `@nicoddemus`_ for the PR.
* Replace ``raise StopIteration`` usages in the code by simple ``returns`` to finish generators, in accordance to `PEP-479`_ (`#2160`_).
- Thanks `@tgoodlet`_ for the report and `@nicoddemus`_ for the PR.
+ Thanks to `@nicoddemus`_ for the PR.
* Fix internal errors when an unprintable ``AssertionError`` is raised inside a test.
Thanks `@omerhadari`_ for the PR.
@@ -3706,7 +3724,7 @@ Bug Fixes
.. _@syre: https://github.com/syre
.. _@adler-j: https://github.com/adler-j
-.. _@d-b-w: https://bitbucket.org/d-b-w/
+.. _@d-b-w: https://github.com/d-b-w
.. _@DuncanBetts: https://github.com/DuncanBetts
.. _@dupuy: https://bitbucket.org/dupuy/
.. _@kerrick-lyft: https://github.com/kerrick-lyft
@@ -3766,7 +3784,7 @@ Bug Fixes
.. _@adborden: https://github.com/adborden
.. _@cwitty: https://github.com/cwitty
-.. _@d_b_w: https://github.com/d_b_w
+.. _@d_b_w: https://github.com/d-b-w
.. _@gdyuldin: https://github.com/gdyuldin
.. _@matclab: https://github.com/matclab
.. _@MSeifert04: https://github.com/MSeifert04
@@ -3801,7 +3819,7 @@ Bug Fixes
Thanks `@axil`_ for the PR.
* Explain a bad scope value passed to ``@fixture`` declarations or
- a ``MetaFunc.parametrize()`` call. Thanks `@tgoodlet`_ for the PR.
+ a ``MetaFunc.parametrize()`` call.
* This version includes ``pluggy-0.4.0``, which correctly handles
``VersionConflict`` errors in plugins (`#704`_).
@@ -3811,7 +3829,6 @@ Bug Fixes
.. _@philpep: https://github.com/philpep
.. _@raquel-ucl: https://github.com/raquel-ucl
.. _@axil: https://github.com/axil
-.. _@tgoodlet: https://github.com/tgoodlet
.. _@vlad-dragos: https://github.com/vlad-dragos
.. _#1853: https://github.com/pytest-dev/pytest/issues/1853
@@ -4157,7 +4174,7 @@ time or change existing behaviors in order to make them less surprising/more use
* Updated docstrings with a more uniform style.
* Add stderr write for ``pytest.exit(msg)`` during startup. Previously the message was never shown.
- Thanks `@BeyondEvil`_ for reporting `#1210`_. Thanks to `@JonathonSonesen`_ and
+ Thanks `@BeyondEvil`_ for reporting `#1210`_. Thanks to `@jgsonesen`_ and
`@tomviner`_ for the PR.
* No longer display the incorrect test deselection reason (`#1372`_).
@@ -4205,7 +4222,7 @@ time or change existing behaviors in order to make them less surprising/more use
Thanks to `@Stranger6667`_ for the PR.
* Fixed the total tests tally in junit xml output (`#1798`_).
- Thanks to `@cryporchild`_ for the PR.
+ Thanks to `@cboelsen`_ for the PR.
* Fixed off-by-one error with lines from ``request.node.warn``.
Thanks to `@blueyed`_ for the PR.
@@ -4278,7 +4295,7 @@ time or change existing behaviors in order to make them less surprising/more use
.. _@BeyondEvil: https://github.com/BeyondEvil
.. _@blueyed: https://github.com/blueyed
.. _@ceridwen: https://github.com/ceridwen
-.. _@cryporchild: https://github.com/cryporchild
+.. _@cboelsen: https://github.com/cboelsen
.. _@csaftoiu: https://github.com/csaftoiu
.. _@d6e: https://github.com/d6e
.. _@davehunt: https://github.com/davehunt
@@ -4289,7 +4306,7 @@ time or change existing behaviors in order to make them less surprising/more use
.. _@gprasad84: https://github.com/gprasad84
.. _@graingert: https://github.com/graingert
.. _@hartym: https://github.com/hartym
-.. _@JonathonSonesen: https://github.com/JonathonSonesen
+.. _@jgsonesen: https://github.com/jgsonesen
.. _@kalekundert: https://github.com/kalekundert
.. _@kvas-it: https://github.com/kvas-it
.. _@marscher: https://github.com/marscher
@@ -4426,7 +4443,7 @@ time or change existing behaviors in order to make them less surprising/more use
**Changes**
-* **Important**: `py.code `_ has been
+* **Important**: `py.code `_ has been
merged into the ``pytest`` repository as ``pytest._code``. This decision
was made because ``py.code`` had very few uses outside ``pytest`` and the
fact that it was in a different repository made it difficult to fix bugs on
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 128df6661..5ef418e0b 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -5,8 +5,9 @@ Contribution getting started
Contributions are highly welcomed and appreciated. Every little help counts,
so do not hesitate!
-.. contents:: Contribution links
+.. contents::
:depth: 2
+ :backlinks: none
.. _submitfeedback:
diff --git a/OPENCOLLECTIVE.rst b/OPENCOLLECTIVE.rst
index 9b7ddcd06..8c1c90281 100644
--- a/OPENCOLLECTIVE.rst
+++ b/OPENCOLLECTIVE.rst
@@ -9,7 +9,7 @@ What is it
==========
Open Collective is an online funding platform for open and transparent communities.
-It provide tools to raise money and share your finances in full transparency.
+It provides tools to raise money and share your finances in full transparency.
It is the platform of choice for individuals and companies that want to make one-time or
monthly donations directly to the project.
@@ -19,7 +19,7 @@ Funds
The OpenCollective funds donated to pytest will be used to fund overall maintenance,
local sprints, merchandising (stickers to distribute in conferences for example), and future
-gatherings of pytest developers (Sprints).
+gatherings of pytest developers (sprints).
`Core contributors`_ which are contributing on a continuous basis are free to submit invoices
to bill maintenance hours using the platform. How much each contributor should request is still an
diff --git a/README.rst b/README.rst
index 9739a1bda..301e49538 100644
--- a/README.rst
+++ b/README.rst
@@ -111,13 +111,13 @@ Consult the `Changelog `__ pag
Support pytest
--------------
-You can support pytest by obtaining a `Tideflift subscription`_.
+You can support pytest by obtaining a `Tidelift subscription`_.
Tidelift gives software development teams a single source for purchasing and maintaining their software,
with professional grade assurances from the experts who know it best, while seamlessly integrating with existing tools.
-.. _`Tideflift subscription`: https://tidelift.com/subscription/pkg/pypi-pytest?utm_source=pypi-pytest&utm_medium=referral&utm_campaign=readme
+.. _`Tidelift subscription`: https://tidelift.com/subscription/pkg/pypi-pytest?utm_source=pypi-pytest&utm_medium=referral&utm_campaign=readme
Security
diff --git a/changelog/5516.trivial.rst b/changelog/5516.trivial.rst
new file mode 100644
index 000000000..2f6b4e35e
--- /dev/null
+++ b/changelog/5516.trivial.rst
@@ -0,0 +1 @@
+Cache node splitting function which can improve collection performance in very large test suites.
diff --git a/changelog/5524.bugfix.rst b/changelog/5524.bugfix.rst
new file mode 100644
index 000000000..96ebbd43e
--- /dev/null
+++ b/changelog/5524.bugfix.rst
@@ -0,0 +1,2 @@
+Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only,
+which could lead to pytest crashing when executed a second time with the ``--basetemp`` option.
diff --git a/changelog/5578.bugfix.rst b/changelog/5578.bugfix.rst
new file mode 100644
index 000000000..5f6c39185
--- /dev/null
+++ b/changelog/5578.bugfix.rst
@@ -0,0 +1,3 @@
+Improve type checking for some exception-raising functions (``pytest.xfail``, ``pytest.skip``, etc)
+so they provide better error messages when users meant to use marks (for example ``@pytest.xfail``
+instead of ``@pytest.mark.xfail``).
diff --git a/changelog/5606.bugfix.rst b/changelog/5606.bugfix.rst
new file mode 100644
index 000000000..82332ba99
--- /dev/null
+++ b/changelog/5606.bugfix.rst
@@ -0,0 +1,2 @@
+Fixed internal error when test functions were patched with objects that cannot be compared
+for truth values against others, like ``numpy`` arrays.
diff --git a/changelog/5634.bugfix.rst b/changelog/5634.bugfix.rst
new file mode 100644
index 000000000..a2a282f93
--- /dev/null
+++ b/changelog/5634.bugfix.rst
@@ -0,0 +1,2 @@
+``pytest.exit`` is now correctly handled in ``unittest`` cases.
+This makes ``unittest`` cases handle ``quit`` from pytest's pdb correctly.
diff --git a/changelog/5650.bugfix.rst b/changelog/5650.bugfix.rst
new file mode 100644
index 000000000..db57a40b9
--- /dev/null
+++ b/changelog/5650.bugfix.rst
@@ -0,0 +1 @@
+Improved output when parsing an ini configuration file fails.
diff --git a/changelog/5664.trivial.rst b/changelog/5664.trivial.rst
new file mode 100644
index 000000000..3928454ef
--- /dev/null
+++ b/changelog/5664.trivial.rst
@@ -0,0 +1,2 @@
+When invoking pytest's own testsuite with ``PYTHONDONTWRITEBYTECODE=1``,
+the ``test_xfail_handling`` test no longer fails.
diff --git a/doc/en/_templates/globaltoc.html b/doc/en/_templates/globaltoc.html
index 39cebb968..50c2239e5 100644
--- a/doc/en/_templates/globaltoc.html
+++ b/doc/en/_templates/globaltoc.html
@@ -4,7 +4,7 @@
Home
Install
Contents
- Reference
+ API Reference
Examples
Customize
Changelog
diff --git a/doc/en/_themes/flask/layout.html b/doc/en/_themes/flask/layout.html
index 19c43fbbe..f2fa8e6aa 100644
--- a/doc/en/_themes/flask/layout.html
+++ b/doc/en/_themes/flask/layout.html
@@ -16,7 +16,7 @@
{%- block footer %}
{% if pagename == 'index' %}
diff --git a/doc/en/_themes/flask/slim_searchbox.html b/doc/en/_themes/flask/slim_searchbox.html
new file mode 100644
index 000000000..e98ad4ed9
--- /dev/null
+++ b/doc/en/_themes/flask/slim_searchbox.html
@@ -0,0 +1,15 @@
+{#
+ basic/searchbox.html with heading removed.
+#}
+{%- if pagename != "search" and builder != "singlehtml" %}
+
+
+{%- endif %}
diff --git a/doc/en/_themes/flask/static/flasky.css_t b/doc/en/_themes/flask/static/flasky.css_t
index 6b593da29..108c85401 100644
--- a/doc/en/_themes/flask/static/flasky.css_t
+++ b/doc/en/_themes/flask/static/flasky.css_t
@@ -8,11 +8,12 @@
{% set page_width = '1020px' %}
{% set sidebar_width = '220px' %}
-/* orange of logo is #d67c29 but we use black for links for now */
-{% set link_color = '#000' %}
-{% set link_hover_color = '#000' %}
+/* muted version of green logo color #C9D22A */
+{% set link_color = '#606413' %}
+/* blue logo color */
+{% set link_hover_color = '#009de0' %}
{% set base_font = 'sans-serif' %}
-{% set header_font = 'serif' %}
+{% set header_font = 'sans-serif' %}
@import url("basic.css");
@@ -20,7 +21,7 @@
body {
font-family: {{ base_font }};
- font-size: 17px;
+ font-size: 16px;
background-color: white;
color: #000;
margin: 0;
@@ -78,13 +79,13 @@ div.related {
}
div.sphinxsidebar a {
- color: #444;
text-decoration: none;
- border-bottom: 1px dotted #999;
+ border-bottom: none;
}
div.sphinxsidebar a:hover {
- border-bottom: 1px solid #999;
+ color: {{ link_hover_color }};
+ border-bottom: 1px solid {{ link_hover_color }};
}
div.sphinxsidebar {
@@ -106,14 +107,14 @@ div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family: {{ header_font }};
color: #444;
- font-size: 24px;
+ font-size: 21px;
font-weight: normal;
- margin: 0 0 5px 0;
+ margin: 16px 0 0 0;
padding: 0;
}
div.sphinxsidebar h4 {
- font-size: 20px;
+ font-size: 18px;
}
div.sphinxsidebar h3 a {
@@ -205,10 +206,22 @@ div.body p, div.body dd, div.body li {
line-height: 1.4em;
}
+ul.simple li {
+ margin-bottom: 0.5em;
+}
+
+div.topic ul.simple li {
+ margin-bottom: 0;
+}
+
+div.topic li > p:first-child {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
div.admonition {
background: #fafafa;
- margin: 20px -30px;
- padding: 10px 30px;
+ padding: 10px 20px;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
@@ -217,11 +230,6 @@ div.admonition tt.xref, div.admonition a tt {
border-bottom: 1px solid #fafafa;
}
-dd div.admonition {
- margin-left: -60px;
- padding-left: 60px;
-}
-
div.admonition p.admonition-title {
font-family: {{ header_font }};
font-weight: normal;
@@ -231,7 +239,7 @@ div.admonition p.admonition-title {
line-height: 1;
}
-div.admonition p.last {
+div.admonition :last-child {
margin-bottom: 0;
}
@@ -243,7 +251,7 @@ dt:target, .highlight {
background: #FAF3E8;
}
-div.note {
+div.note, div.warning {
background-color: #eee;
border: 1px solid #ccc;
}
@@ -257,6 +265,11 @@ div.topic {
background-color: #eee;
}
+div.topic a {
+ text-decoration: none;
+ border-bottom: none;
+}
+
p.admonition-title {
display: inline;
}
@@ -358,21 +371,10 @@ ul, ol {
pre {
background: #eee;
- padding: 7px 30px;
- margin: 15px -30px;
+ padding: 7px 12px;
line-height: 1.3em;
}
-dl pre, blockquote pre, li pre {
- margin-left: -60px;
- padding-left: 60px;
-}
-
-dl dl pre {
- margin-left: -90px;
- padding-left: 90px;
-}
-
tt {
background-color: #ecf0f3;
color: #222;
@@ -393,6 +395,20 @@ a.reference:hover {
border-bottom: 1px solid {{ link_hover_color }};
}
+li.toctree-l1 a.reference,
+li.toctree-l2 a.reference,
+li.toctree-l3 a.reference,
+li.toctree-l4 a.reference {
+ border-bottom: none;
+}
+
+li.toctree-l1 a.reference:hover,
+li.toctree-l2 a.reference:hover,
+li.toctree-l3 a.reference:hover,
+li.toctree-l4 a.reference:hover {
+ border-bottom: 1px solid {{ link_hover_color }};
+}
+
a.footnote-reference {
text-decoration: none;
font-size: 0.7em;
@@ -408,6 +424,56 @@ a:hover tt {
background: #EEE;
}
+#reference div.section h2 {
+ /* separate code elements in the reference section */
+ border-top: 2px solid #ccc;
+ padding-top: 0.5em;
+}
+
+#reference div.section h3 {
+ /* separate code elements in the reference section */
+ border-top: 1px solid #ccc;
+ padding-top: 0.5em;
+}
+
+dl.class, dl.function {
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+dl.class > dd {
+ border-left: 3px solid #ccc;
+ margin-left: 0px;
+ padding-left: 30px;
+}
+
+dl.field-list {
+ flex-direction: column;
+}
+
+dl.field-list dd {
+ padding-left: 4em;
+ border-left: 3px solid #ccc;
+ margin-bottom: 0.5em;
+}
+
+dl.field-list dd > ul {
+ list-style: none;
+ padding-left: 0px;
+}
+
+dl.field-list dd > ul > li li :first-child {
+ text-indent: 0;
+}
+
+dl.field-list dd > ul > li :first-child {
+ text-indent: -2em;
+ padding-left: 0px;
+}
+
+dl.field-list dd > p:first-child {
+ text-indent: -2em;
+}
@media screen and (max-width: 870px) {
diff --git a/doc/en/adopt.rst b/doc/en/adopt.rst
index 710f431be..e3c0477bc 100644
--- a/doc/en/adopt.rst
+++ b/doc/en/adopt.rst
@@ -24,11 +24,9 @@ The ideal pytest helper
- feels confident in using pytest (e.g. has explored command line options, knows how to write parametrized tests, has an idea about conftest contents)
- does not need to be an expert in every aspect!
-`Pytest helpers, sign up here`_! (preferably in February, hard deadline 22 March)
+Pytest helpers, sign up here! (preferably in February, hard deadline 22 March)
-.. _`Pytest helpers, sign up here`: http://goo.gl/forms/nxqAhqWt1P
-
The ideal partner project
-----------------------------------------
@@ -40,11 +38,9 @@ The ideal partner project
- has the support of the core development team, in trying out pytest adoption
- has no tests... or 100% test coverage... or somewhere in between!
-`Partner projects, sign up here`_! (by 22 March)
+Partner projects, sign up here! (by 22 March)
-.. _`Partner projects, sign up here`: http://goo.gl/forms/ZGyqlHiwk3
-
What does it mean to "adopt pytest"?
-----------------------------------------
@@ -68,11 +64,11 @@ Progressive success might look like:
It may be after the month is up, the partner project decides that pytest is not right for it. That's okay - hopefully the pytest team will also learn something about its weaknesses or deficiencies.
.. _`nose and unittest`: faq.html#how-does-pytest-relate-to-nose-and-unittest
-.. _assert: asserts.html
+.. _assert: assert.html
.. _pycmd: https://bitbucket.org/hpk42/pycmd/overview
.. _`setUp/tearDown methods`: xunit_setup.html
.. _fixtures: fixture.html
-.. _markers: markers.html
+.. _markers: mark.html
.. _distributed: xdist.html
diff --git a/doc/en/announce/release-2.1.0.rst b/doc/en/announce/release-2.1.0.rst
index 831548ac2..2a2181d97 100644
--- a/doc/en/announce/release-2.1.0.rst
+++ b/doc/en/announce/release-2.1.0.rst
@@ -12,7 +12,7 @@ courtesy of Benjamin Peterson. You can now safely use ``assert``
statements in test modules without having to worry about side effects
or python optimization ("-OO") options. This is achieved by rewriting
assert statements in test modules upon import, using a PEP302 hook.
-See http://pytest.org/assert.html#advanced-assertion-introspection for
+See https://docs.pytest.org/en/latest/assert.html for
detailed information. The work has been partly sponsored by my company,
merlinux GmbH.
diff --git a/doc/en/announce/release-2.9.0.rst b/doc/en/announce/release-2.9.0.rst
index c079fdf6b..05d9a394f 100644
--- a/doc/en/announce/release-2.9.0.rst
+++ b/doc/en/announce/release-2.9.0.rst
@@ -75,7 +75,7 @@ The py.test Development Team
**Changes**
-* **Important**: `py.code `_ has been
+* **Important**: `py.code `_ has been
merged into the ``pytest`` repository as ``pytest._code``. This decision
was made because ``py.code`` had very few uses outside ``pytest`` and the
fact that it was in a different repository made it difficult to fix bugs on
@@ -88,7 +88,7 @@ The py.test Development Team
**experimental**, so you definitely should not import it explicitly!
Please note that the original ``py.code`` is still available in
- `pylib `_.
+ `pylib `_.
* ``pytest_enter_pdb`` now optionally receives the pytest config object.
Thanks `@nicoddemus`_ for the PR.
diff --git a/doc/en/announce/release-2.9.2.rst b/doc/en/announce/release-2.9.2.rst
index 8f274cdf3..b007a6d99 100644
--- a/doc/en/announce/release-2.9.2.rst
+++ b/doc/en/announce/release-2.9.2.rst
@@ -66,8 +66,8 @@ The py.test Development Team
.. _#510: https://github.com/pytest-dev/pytest/issues/510
.. _#1506: https://github.com/pytest-dev/pytest/pull/1506
-.. _#1496: https://github.com/pytest-dev/pytest/issue/1496
-.. _#1524: https://github.com/pytest-dev/pytest/issue/1524
+.. _#1496: https://github.com/pytest-dev/pytest/issues/1496
+.. _#1524: https://github.com/pytest-dev/pytest/pull/1524
.. _@astraw38: https://github.com/astraw38
.. _@hackebrot: https://github.com/hackebrot
diff --git a/doc/en/conf.py b/doc/en/conf.py
index 42dc18fd8..1a6ef7ca8 100644
--- a/doc/en/conf.py
+++ b/doc/en/conf.py
@@ -15,7 +15,6 @@
#
# The full version, including alpha/beta/rc tags.
# The short X.Y version.
-import datetime
import os
import sys
@@ -63,8 +62,7 @@ master_doc = "contents"
# General information about the project.
project = "pytest"
-year = datetime.datetime.utcnow().year
-copyright = "2015–2019 , holger krekel and pytest-dev team"
+copyright = "2015–2019, holger krekel and pytest-dev team"
# The language for content autogenerated by Sphinx. Refer to documentation
@@ -167,18 +165,18 @@ html_favicon = "img/pytest1favi.ico"
html_sidebars = {
"index": [
+ "slim_searchbox.html",
"sidebarintro.html",
"globaltoc.html",
"links.html",
"sourcelink.html",
- "searchbox.html",
],
"**": [
+ "slim_searchbox.html",
"globaltoc.html",
"relations.html",
"links.html",
"sourcelink.html",
- "searchbox.html",
],
}
diff --git a/doc/en/example/fixtures/test_fixtures_order.py b/doc/en/example/fixtures/test_fixtures_order.py
new file mode 100644
index 000000000..97b3e8005
--- /dev/null
+++ b/doc/en/example/fixtures/test_fixtures_order.py
@@ -0,0 +1,38 @@
+import pytest
+
+# fixtures documentation order example
+order = []
+
+
+@pytest.fixture(scope="session")
+def s1():
+ order.append("s1")
+
+
+@pytest.fixture(scope="module")
+def m1():
+ order.append("m1")
+
+
+@pytest.fixture
+def f1(f3):
+ order.append("f1")
+
+
+@pytest.fixture
+def f3():
+ order.append("f3")
+
+
+@pytest.fixture(autouse=True)
+def a1():
+ order.append("a1")
+
+
+@pytest.fixture
+def f2():
+ order.append("f2")
+
+
+def test_order(f1, m1, f2, s1):
+ assert order == ["s1", "m1", "a1", "f3", "f1", "f2"]
diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst
index 004e77add..218ef2707 100644
--- a/doc/en/example/markers.rst
+++ b/doc/en/example/markers.rst
@@ -336,7 +336,7 @@ apply a marker to an individual test instance:
@pytest.mark.foo
@pytest.mark.parametrize(
- ("n", "expected"), [(1, 2), pytest.param((1, 3), marks=pytest.mark.bar), (2, 3)]
+ ("n", "expected"), [(1, 2), pytest.param(1, 3, marks=pytest.mark.bar), (2, 3)]
)
def test_increment(n, expected):
assert n + 1 == expected
diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst
index 0a5ba8358..3dff2c559 100644
--- a/doc/en/example/parametrize.rst
+++ b/doc/en/example/parametrize.rst
@@ -552,13 +552,13 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker:
platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
- collecting ... collected 17 items / 14 deselected / 3 selected
+ collecting ... collected 18 items / 15 deselected / 3 selected
test_pytest_param_example.py::test_eval[1+7-8] PASSED [ 33%]
test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%]
test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%]
- ============ 2 passed, 14 deselected, 1 xfailed in 0.12 seconds ============
+ ============ 2 passed, 15 deselected, 1 xfailed in 0.12 seconds ============
As the result:
diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst
index 852069731..40493e66f 100644
--- a/doc/en/fixture.rst
+++ b/doc/en/fixture.rst
@@ -289,51 +289,29 @@ are finalized when the last test of a *package* finishes.
Use this new feature sparingly and please make sure to report any issues you find.
-Higher-scoped fixtures are instantiated first
----------------------------------------------
+Order: Higher-scoped fixtures are instantiated first
+----------------------------------------------------
Within a function request for features, fixture of higher-scopes (such as ``session``) are instantiated first than
lower-scoped fixtures (such as ``function`` or ``class``). The relative order of fixtures of same scope follows
-the declared order in the test function and honours dependencies between fixtures.
+the declared order in the test function and honours dependencies between fixtures. Autouse fixtures will be
+instantiated before explicitly used fixtures.
Consider the code below:
-.. code-block:: python
-
- @pytest.fixture(scope="session")
- def s1():
- pass
-
-
- @pytest.fixture(scope="module")
- def m1():
- pass
-
-
- @pytest.fixture
- def f1(tmpdir):
- pass
-
-
- @pytest.fixture
- def f2():
- pass
-
-
- def test_foo(f1, m1, f2, s1):
- ...
-
+.. literalinclude:: example/fixtures/test_fixtures_order.py
The fixtures requested by ``test_foo`` will be instantiated in the following order:
1. ``s1``: is the highest-scoped fixture (``session``).
2. ``m1``: is the second highest-scoped fixture (``module``).
-3. ``tmpdir``: is a ``function``-scoped fixture, required by ``f1``: it needs to be instantiated at this point
- because it is a dependency of ``f1``.
-4. ``f1``: is the first ``function``-scoped fixture in ``test_foo`` parameter list.
-5. ``f2``: is the last ``function``-scoped fixture in ``test_foo`` parameter list.
+3. ``a1``: is a ``function``-scoped ``autouse`` fixture: it will be instantiated before other fixtures
+ within the same scope.
+4. ``f3``: is a ``function``-scoped fixture, required by ``f1``: it needs to be instantiated at this point
+5. ``f1``: is the first ``function``-scoped fixture in ``test_foo`` parameter list.
+6. ``f2``: is the last ``function``-scoped fixture in ``test_foo`` parameter list.
.. _`finalization`:
@@ -400,6 +378,34 @@ The ``smtp_connection`` connection will be closed after the test finished
execution because the ``smtp_connection`` object automatically closes when
the ``with`` statement ends.
+Using the contextlib.ExitStack context manager finalizers will always be called
+regardless if the fixture *setup* code raises an exception. This is handy to properly
+close all resources created by a fixture even if one of them fails to be created/acquired:
+
+.. code-block:: python
+
+ # content of test_yield3.py
+
+ import contextlib
+
+ import pytest
+
+
+ @contextlib.contextmanager
+ def connect(port):
+ ... # create connection
+ yield
+ ... # close connection
+
+
+ @pytest.fixture
+ def equipments():
+ with contextlib.ExitStack() as stack:
+ yield [stack.enter_context(connect(port)) for port in ("C1", "C3", "C28")]
+
+In the example above, if ``"C28"`` fails with an exception, ``"C1"`` and ``"C3"`` will still
+be properly closed.
+
Note that if an exception happens during the *setup* code (before the ``yield`` keyword), the
*teardown* code (after the ``yield``) will not be called.
@@ -428,27 +434,39 @@ Here's the ``smtp_connection`` fixture changed to use ``addfinalizer`` for clean
return smtp_connection # provide the fixture value
+Here's the ``equipments`` fixture changed to use ``addfinalizer`` for cleanup:
+
+.. code-block:: python
+
+ # content of test_yield3.py
+
+ import contextlib
+ import functools
+
+ import pytest
+
+
+ @contextlib.contextmanager
+ def connect(port):
+ ... # create connection
+ yield
+ ... # close connection
+
+
+ @pytest.fixture
+ def equipments(request):
+ r = []
+ for port in ("C1", "C3", "C28"):
+ cm = connect(port)
+ equip = cm.__enter__()
+ request.addfinalizer(functools.partial(cm.__exit__, None, None, None))
+ r.append(equip)
+ return r
+
+
Both ``yield`` and ``addfinalizer`` methods work similarly by calling their code after the test
-ends, but ``addfinalizer`` has two key differences over ``yield``:
-
-1. It is possible to register multiple finalizer functions.
-
-2. Finalizers will always be called regardless if the fixture *setup* code raises an exception.
- This is handy to properly close all resources created by a fixture even if one of them
- fails to be created/acquired::
-
- @pytest.fixture
- def equipments(request):
- r = []
- for port in ('C1', 'C3', 'C28'):
- equip = connect(port)
- request.addfinalizer(equip.disconnect)
- r.append(equip)
- return r
-
- In the example above, if ``"C28"`` fails with an exception, ``"C1"`` and ``"C3"`` will still
- be properly closed. Of course, if an exception happens before the finalize function is
- registered then it will not be executed.
+ends. Of course, if an exception happens before the finalize function is registered then it
+will not be executed.
.. _`request-context`:
@@ -522,7 +540,7 @@ of a fixture is needed multiple times in a single test. Instead of returning
data directly, the fixture instead returns a function which generates the data.
This function can then be called multiple times in the test.
-Factories can have have parameters as needed::
+Factories can have parameters as needed::
@pytest.fixture
def make_customer_record():
diff --git a/doc/en/flaky.rst b/doc/en/flaky.rst
index 8e340316e..0f0eecab0 100644
--- a/doc/en/flaky.rst
+++ b/doc/en/flaky.rst
@@ -122,4 +122,4 @@ Resources
* Google:
* `Flaky Tests at Google and How We Mitigate Them `_ by John Micco, 2016
- * `Where do Google's flaky tests come from? `_ by Jeff Listfield, 2017
+ * `Where do Google's flaky tests come from? `_ by Jeff Listfield, 2017
diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst
index 750de086e..c313f3849 100644
--- a/doc/en/getting-started.rst
+++ b/doc/en/getting-started.rst
@@ -142,7 +142,7 @@ The first test passed and the second failed. You can easily see the intermediate
Request a unique temporary directory for functional tests
--------------------------------------------------------------
-``pytest`` provides `Builtin fixtures/function arguments `_ to request arbitrary resources, like a unique temporary directory::
+``pytest`` provides `Builtin fixtures/function arguments `_ to request arbitrary resources, like a unique temporary directory::
# content of test_tmpdir.py
def test_needsfiles(tmpdir):
diff --git a/doc/en/index.rst b/doc/en/index.rst
index 2d6ea620f..6c7c84865 100644
--- a/doc/en/index.rst
+++ b/doc/en/index.rst
@@ -61,7 +61,7 @@ Features
- Can run :ref:`unittest ` (including trial) and :ref:`nose ` test suites out of the box;
-- Python Python 3.5+ and PyPy 3;
+- Python 3.5+ and PyPy 3;
- Rich plugin architecture, with over 315+ `external plugins `_ and thriving community;
diff --git a/doc/en/license.rst b/doc/en/license.rst
index 5ee55cf96..d94b34a99 100644
--- a/doc/en/license.rst
+++ b/doc/en/license.rst
@@ -9,7 +9,7 @@ Distributed under the terms of the `MIT`_ license, pytest is free and open sourc
The MIT License (MIT)
- Copyright (c) 2004-2017 Holger Krekel and others
+ Copyright (c) 2004-2019 Holger Krekel and others
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
diff --git a/doc/en/links.inc b/doc/en/links.inc
index 1b7cbd05b..7c5e8d88c 100644
--- a/doc/en/links.inc
+++ b/doc/en/links.inc
@@ -14,7 +14,7 @@
.. _`distribute docs`:
.. _`distribute`: https://pypi.org/project/distribute/
.. _`pip`: https://pypi.org/project/pip/
-.. _`venv`: https://docs.python.org/3/library/venv.html/
+.. _`venv`: https://docs.python.org/3/library/venv.html
.. _`virtualenv`: https://pypi.org/project/virtualenv/
.. _hudson: http://hudson-ci.org/
.. _jenkins: http://jenkins-ci.org/
diff --git a/doc/en/mark.rst b/doc/en/mark.rst
index de6ab7822..3899dab88 100644
--- a/doc/en/mark.rst
+++ b/doc/en/mark.rst
@@ -40,7 +40,7 @@ You can register custom marks in your ``pytest.ini`` file like this:
Note that everything after the ``:`` is an optional description.
-Alternatively, you can register new markers programatically in a
+Alternatively, you can register new markers programmatically in a
:ref:`pytest_configure ` hook:
.. code-block:: python
diff --git a/doc/en/monkeypatch.rst b/doc/en/monkeypatch.rst
index 8e4622982..fd277d234 100644
--- a/doc/en/monkeypatch.rst
+++ b/doc/en/monkeypatch.rst
@@ -46,10 +46,13 @@ environment variable is missing, or to set multiple values to a known variable.
:py:meth:`monkeypatch.setenv` and :py:meth:`monkeypatch.delenv` can be used for
these patches.
-4. Use :py:meth:`monkeypatch.syspath_prepend` to modify the system ``$PATH`` safely, and
+4. Use ``monkeypatch.setenv("PATH", value, prepend=os.pathsep)`` to modify ``$PATH``, and
:py:meth:`monkeypatch.chdir` to change the context of the current working directory
during a test.
+5. Use py:meth:`monkeypatch.syspath_prepend` to modify ``sys.path`` which will also
+call :py:meth:`pkg_resources.fixup_namespace_packages` and :py:meth:`importlib.invalidate_caches`.
+
See the `monkeypatch blog post`_ for some introduction material
and a discussion of its motivation.
diff --git a/doc/en/projects.rst b/doc/en/projects.rst
index 606e9d47c..226358596 100644
--- a/doc/en/projects.rst
+++ b/doc/en/projects.rst
@@ -28,7 +28,6 @@ Here are some examples of projects using ``pytest`` (please send notes via :ref:
* `sentry `_, realtime app-maintenance and exception tracking
* `Astropy `_ and `affiliated packages `_
* `tox `_, virtualenv/Hudson integration tool
-* `PIDA `_ framework for integrated development
* `PyPM `_ ActiveState's package manager
* `Fom `_ a fluid object mapper for FluidDB
* `applib `_ cross-platform utilities
@@ -37,8 +36,7 @@ Here are some examples of projects using ``pytest`` (please send notes via :ref:
* `mwlib `_ mediawiki parser and utility library
* `The Translate Toolkit `_ for localization and conversion
* `execnet `_ rapid multi-Python deployment
-* `pylib `_ cross-platform path, IO, dynamic code library
-* `Pacha `_ configuration management in five minutes
+* `pylib `_ cross-platform path, IO, dynamic code library
* `bbfreeze `_ create standalone executables from Python scripts
* `pdb++ `_ a fancier version of PDB
* `py-s3fuse `_ Amazon S3 FUSE based filesystem
@@ -77,7 +75,7 @@ Some organisations using pytest
* `Tandberg `_
* `Shootq `_
* `Stups department of Heinrich Heine University Duesseldorf `_
-* `cellzome `_
+* cellzome
* `Open End, Gothenborg `_
* `Laboratory of Bioinformatics, Warsaw `_
* `merlinux, Germany `_
diff --git a/doc/en/reference.rst b/doc/en/reference.rst
index 5abb01f50..afbef6b1e 100644
--- a/doc/en/reference.rst
+++ b/doc/en/reference.rst
@@ -1,5 +1,5 @@
-Reference
-=========
+API Reference
+=============
This page contains the full reference to pytest's API.
diff --git a/doc/en/talks.rst b/doc/en/talks.rst
index 20b6e5b09..f66192817 100644
--- a/doc/en/talks.rst
+++ b/doc/en/talks.rst
@@ -4,9 +4,8 @@ Talks and Tutorials
.. sidebar:: Next Open Trainings
- - `Training at Europython 2019 `_, 8th July 2019, Basel, Switzerland.
-
- `Training at Workshoptage 2019 `_ (German), 10th September 2019, Rapperswil, Switzerland.
+ - `3 day hands-on workshop covering pytest, tox and devpi: "Professional Testing with Python" `_ (English), October 21 - 23, 2019, Leipzig, Germany.
.. _`funcargs`: funcargs.html
diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst
index 5934e035d..3c1f37f41 100644
--- a/doc/en/warnings.rst
+++ b/doc/en/warnings.rst
@@ -127,7 +127,7 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable:
*Credits go to Florian Schulze for the reference implementation in the* `pytest-warnings`_
*plugin.*
-.. _`-W option`: https://docs.python.org/3/using/cmdline.html?highlight=#cmdoption-W
+.. _`-W option`: https://docs.python.org/3/using/cmdline.html#cmdoption-w
.. _warnings.simplefilter: https://docs.python.org/3/library/warnings.html#warnings.simplefilter
.. _`pytest-warnings`: https://github.com/fschulze/pytest-warnings
diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst
index 4bbc1afce..67a8fcecf 100644
--- a/doc/en/writing_plugins.rst
+++ b/doc/en/writing_plugins.rst
@@ -164,7 +164,7 @@ If a package is installed this way, ``pytest`` will load
.. note::
Make sure to include ``Framework :: Pytest`` in your list of
- `PyPI classifiers `_
+ `PyPI classifiers `_
to make it easy for users to find your plugin.
diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py
index 17463959f..496931e0f 100755
--- a/src/_pytest/cacheprovider.py
+++ b/src/_pytest/cacheprovider.py
@@ -14,7 +14,7 @@ import py
import pytest
from .pathlib import Path
from .pathlib import resolve_from_str
-from .pathlib import rmtree
+from .pathlib import rm_rf
README_CONTENT = """\
# pytest cache directory #
@@ -44,7 +44,7 @@ class Cache:
def for_config(cls, config):
cachedir = cls.cache_dir_from_config(config)
if config.getoption("cacheclear") and cachedir.exists():
- rmtree(cachedir, force=True)
+ rm_rf(cachedir)
cachedir.mkdir()
return cls(cachedir, config)
diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py
index 0525d6985..df049991b 100644
--- a/src/_pytest/compat.py
+++ b/src/_pytest/compat.py
@@ -70,13 +70,18 @@ def num_mock_patch_args(function):
patchings = getattr(function, "patchings", None)
if not patchings:
return 0
- mock_modules = [sys.modules.get("mock"), sys.modules.get("unittest.mock")]
- if any(mock_modules):
- sentinels = [m.DEFAULT for m in mock_modules if m is not None]
- return len(
- [p for p in patchings if not p.attribute_name and p.new in sentinels]
- )
- return len(patchings)
+
+ mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object())
+ ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object())
+
+ return len(
+ [
+ p
+ for p in patchings
+ if not p.attribute_name
+ and (p.new is mock_sentinel or p.new is ut_mock_sentinel)
+ ]
+ )
def getfuncargnames(function, is_method=False, cls=None):
diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py
index 43cf62ab1..8994ff7d9 100644
--- a/src/_pytest/config/argparsing.py
+++ b/src/_pytest/config/argparsing.py
@@ -399,7 +399,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
"""shorten help for long options that differ only in extra hyphens
- collapse **long** options that are the same except for extra hyphens
- - special action attribute map_long_option allows surpressing additional
+ - special action attribute map_long_option allows suppressing additional
long options
- shortcut if there are only two options and one of them is a short one
- cache result on action object as this is called at least 2 times
diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py
index abb248b1d..ec991316a 100644
--- a/src/_pytest/config/findpaths.py
+++ b/src/_pytest/config/findpaths.py
@@ -30,7 +30,11 @@ def getcfg(args, config=None):
for inibasename in inibasenames:
p = base.join(inibasename)
if exists(p):
- iniconfig = py.iniconfig.IniConfig(p)
+ try:
+ iniconfig = py.iniconfig.IniConfig(p)
+ except py.iniconfig.ParseError as exc:
+ raise UsageError(str(exc))
+
if (
inibasename == "setup.cfg"
and "tool:pytest" in iniconfig.sections
diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py
index da4900dc9..332c86bde 100644
--- a/src/_pytest/mark/structures.py
+++ b/src/_pytest/mark/structures.py
@@ -171,9 +171,7 @@ class Mark:
@attr.s
class MarkDecorator:
""" A decorator for test functions and test classes. When applied
- it will create :class:`MarkInfo` objects which may be
- :ref:`retrieved by hooks as item keywords `.
- MarkDecorator instances are often created like this::
+ it will create :class:`Mark` objects which are often created like this::
mark1 = pytest.mark.NAME # simple MarkDecorator
mark2 = pytest.mark.NAME(name1=value) # parametrized MarkDecorator
@@ -185,17 +183,18 @@ class MarkDecorator:
pass
When a MarkDecorator instance is called it does the following:
- 1. If called with a single class as its only positional argument and no
- additional keyword arguments, it attaches itself to the class so it
- gets applied automatically to all test cases found in that class.
- 2. If called with a single function as its only positional argument and
- no additional keyword arguments, it attaches a MarkInfo object to the
- function, containing all the arguments already stored internally in
- the MarkDecorator.
- 3. When called in any other case, it performs a 'fake construction' call,
- i.e. it returns a new MarkDecorator instance with the original
- MarkDecorator's content updated with the arguments passed to this
- call.
+
+ 1. If called with a single class as its only positional argument and no
+ additional keyword arguments, it attaches itself to the class so it
+ gets applied automatically to all test cases found in that class.
+ 2. If called with a single function as its only positional argument and
+ no additional keyword arguments, it attaches a MarkInfo object to the
+ function, containing all the arguments already stored internally in
+ the MarkDecorator.
+ 3. When called in any other case, it performs a 'fake construction' call,
+ i.e. it returns a new MarkDecorator instance with the original
+ MarkDecorator's content updated with the arguments passed to this
+ call.
Note: The rules above prevent MarkDecorator objects from storing only a
single function or class reference as their positional argument with no
diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py
index 7e1c40bcb..9b78dca38 100644
--- a/src/_pytest/nodes.py
+++ b/src/_pytest/nodes.py
@@ -1,5 +1,6 @@
import os
import warnings
+from functools import lru_cache
import py
@@ -13,6 +14,7 @@ SEP = "/"
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
+@lru_cache(maxsize=None)
def _splitnode(nodeid):
"""Split a nodeid into constituent 'parts'.
@@ -30,11 +32,12 @@ def _splitnode(nodeid):
"""
if nodeid == "":
# If there is no root node at all, return an empty list so the caller's logic can remain sane
- return []
+ return ()
parts = nodeid.split(SEP)
# Replace single last element 'test_foo.py::Bar' with multiple elements 'test_foo.py', 'Bar'
parts[-1:] = parts[-1].split("::")
- return parts
+ # Convert parts into a tuple to avoid possible errors with caching of a mutable type
+ return tuple(parts)
def ischildnode(baseid, nodeid):
diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py
index a5a4e655b..5e331a323 100644
--- a/src/_pytest/outcomes.py
+++ b/src/_pytest/outcomes.py
@@ -18,6 +18,12 @@ class OutcomeException(BaseException):
"""
def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
+ if msg is not None and not isinstance(msg, str):
+ error_msg = (
+ "{} expected string as 'msg' parameter, got '{}' instead.\n"
+ "Perhaps you meant to use a mark?"
+ )
+ raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__))
BaseException.__init__(self, msg)
self.msg = msg
self.pytrace = pytrace
diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py
index ecc38eb0f..1c0c45b14 100644
--- a/src/_pytest/pathlib.py
+++ b/src/_pytest/pathlib.py
@@ -7,12 +7,15 @@ import os
import shutil
import sys
import uuid
+import warnings
+from functools import partial
from os.path import expanduser
from os.path import expandvars
from os.path import isabs
from os.path import sep
from posixpath import sep as posix_sep
+from _pytest.warning_types import PytestWarning
if sys.version_info[:2] >= (3, 6):
from pathlib import Path, PurePath
@@ -32,17 +35,53 @@ def ensure_reset_dir(path):
ensures the given path is an empty directory
"""
if path.exists():
- rmtree(path, force=True)
+ rm_rf(path)
path.mkdir()
-def rmtree(path, force=False):
- if force:
- # NOTE: ignore_errors might leave dead folders around.
- # Python needs a rm -rf as a followup.
- shutil.rmtree(str(path), ignore_errors=True)
- else:
- shutil.rmtree(str(path))
+def on_rm_rf_error(func, path: str, exc, *, start_path):
+ """Handles known read-only errors during rmtree."""
+ excvalue = exc[1]
+
+ if not isinstance(excvalue, PermissionError):
+ warnings.warn(
+ PytestWarning("(rm_rf) error removing {}: {}".format(path, excvalue))
+ )
+ return
+
+ if func not in (os.rmdir, os.remove, os.unlink):
+ warnings.warn(
+ PytestWarning("(rm_rf) error removing {}: {}".format(path, excvalue))
+ )
+ return
+
+ # Chmod + retry.
+ import stat
+
+ def chmod_rw(p: str):
+ mode = os.stat(p).st_mode
+ os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR)
+
+ # For files, we need to recursively go upwards in the directories to
+ # ensure they all are also writable.
+ p = Path(path)
+ if p.is_file():
+ for parent in p.parents:
+ chmod_rw(str(parent))
+ # stop when we reach the original path passed to rm_rf
+ if parent == start_path:
+ break
+ chmod_rw(str(path))
+
+ func(path)
+
+
+def rm_rf(path: Path):
+ """Remove the path contents recursively, even if some elements
+ are read-only.
+ """
+ onerror = partial(on_rm_rf_error, start_path=path)
+ shutil.rmtree(str(path), onerror=onerror)
def find_prefixed(root, prefix):
@@ -82,9 +121,9 @@ def _force_symlink(root, target, link_to):
"""helper to create the current symlink
it's full of race conditions that are reasonably ok to ignore
- for the context of best effort linking to the latest testrun
+ for the context of best effort linking to the latest test run
- the presumption being thatin case of much parallelism
+ the presumption being that in case of much parallelism
the inaccuracy is going to be acceptable
"""
current_symlink = root.joinpath(target)
@@ -168,7 +207,7 @@ def maybe_delete_a_numbered_dir(path):
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
path.rename(garbage)
- rmtree(garbage, force=True)
+ rm_rf(garbage)
except (OSError, EnvironmentError):
# known races:
# * other process did a cleanup at the same time
diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py
index 08426d69c..fbc3d914e 100644
--- a/src/_pytest/python_api.py
+++ b/src/_pytest/python_api.py
@@ -579,7 +579,7 @@ def raises(
string that may contain `special characters`__, the pattern can
first be escaped with ``re.escape``.
- __ https://docs.python.org/3/library/re.html#regular-expression-syntax
+ __ https://docs.python.org/3/library/re.html#regular-expression-syntax
.. currentmodule:: _pytest._code
diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py
index a18a58573..6fa21cd1c 100644
--- a/src/_pytest/stepwise.py
+++ b/src/_pytest/stepwise.py
@@ -71,7 +71,6 @@ class StepwisePlugin:
config.hook.pytest_deselected(items=already_passed)
def pytest_runtest_logreport(self, report):
- # Skip this hook if plugin is not active or the test is xfailed.
if not self.active:
return
diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py
index bcd6e1f7c..05d5427c3 100644
--- a/src/_pytest/terminal.py
+++ b/src/_pytest/terminal.py
@@ -692,7 +692,7 @@ class TerminalReporter:
else:
excrepr.reprcrash.toterminal(self._tw)
self._tw.line(
- "(to show a full traceback on KeyboardInterrupt use --fulltrace)",
+ "(to show a full traceback on KeyboardInterrupt use --full-trace)",
yellow=True,
)
diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py
index c9bdf79c1..11dc77cc4 100644
--- a/src/_pytest/unittest.py
+++ b/src/_pytest/unittest.py
@@ -6,6 +6,7 @@ import _pytest._code
import pytest
from _pytest.compat import getimfunc
from _pytest.config import hookimpl
+from _pytest.outcomes import exit
from _pytest.outcomes import fail
from _pytest.outcomes import skip
from _pytest.outcomes import xfail
@@ -154,6 +155,11 @@ class TestCaseFunction(Function):
self.__dict__.setdefault("_excinfo", []).append(excinfo)
def addError(self, testcase, rawexcinfo):
+ try:
+ if isinstance(rawexcinfo[1], exit.Exception):
+ exit(rawexcinfo[1].msg)
+ except TypeError:
+ pass
self._addexcinfo(rawexcinfo)
def addFailure(self, testcase, rawexcinfo):
diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py
index 76f974957..7742b4da9 100644
--- a/testing/code/test_excinfo.py
+++ b/testing/code/test_excinfo.py
@@ -378,7 +378,7 @@ def test_excinfo_no_python_sourcecode(tmpdir):
excinfo = pytest.raises(ValueError, template.render, h=h)
for item in excinfo.traceback:
print(item) # XXX: for some reason jinja.Template.render is printed in full
- item.source # shouldnt fail
+ item.source # shouldn't fail
if item.path.basename == "test.txt":
assert str(item.source) == "{{ h()}}:"
diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py
index 97b88e939..b8a22428f 100644
--- a/testing/deprecated_test.py
+++ b/testing/deprecated_test.py
@@ -36,7 +36,7 @@ def test_terminal_reporter_writer_attr(pytestconfig):
assert terminal_reporter.writer is terminal_reporter._tw
-@pytest.mark.parametrize("plugin", deprecated.DEPRECATED_EXTERNAL_PLUGINS)
+@pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS))
@pytest.mark.filterwarnings("default")
def test_external_plugins_integrated(testdir, plugin):
testdir.syspathinsert()
diff --git a/testing/python/integration.py b/testing/python/integration.py
index 0b87fea33..73419eef4 100644
--- a/testing/python/integration.py
+++ b/testing/python/integration.py
@@ -104,21 +104,15 @@ class TestMockDecoration:
values = getfuncargnames(f)
assert values == ("x",)
- @pytest.mark.xfail(
- strict=False, reason="getfuncargnames breaks if mock is imported"
- )
- def test_wrapped_getfuncargnames_patching(self):
+ def test_getfuncargnames_patching(self):
from _pytest.compat import getfuncargnames
+ from unittest.mock import patch
- def wrap(f):
- def func():
+ class T:
+ def original(self, x, y, z):
pass
- func.__wrapped__ = f
- func.patchings = ["qwe"]
- return func
-
- @wrap
+ @patch.object(T, "original")
def f(x, y, z):
pass
@@ -126,7 +120,6 @@ class TestMockDecoration:
assert values == ("y", "z")
def test_unittest_mock(self, testdir):
- pytest.importorskip("unittest.mock")
testdir.makepyfile(
"""
import unittest.mock
@@ -142,7 +135,6 @@ class TestMockDecoration:
reprec.assertoutcome(passed=1)
def test_unittest_mock_and_fixture(self, testdir):
- pytest.importorskip("unittest.mock")
testdir.makepyfile(
"""
import os.path
@@ -164,7 +156,6 @@ class TestMockDecoration:
reprec.assertoutcome(passed=1)
def test_unittest_mock_and_pypi_mock(self, testdir):
- pytest.importorskip("unittest.mock")
pytest.importorskip("mock", "1.0.1")
testdir.makepyfile(
"""
@@ -187,6 +178,34 @@ class TestMockDecoration:
reprec = testdir.inline_run()
reprec.assertoutcome(passed=2)
+ def test_mock_sentinel_check_against_numpy_like(self, testdir):
+ """Ensure our function that detects mock arguments compares against sentinels using
+ identity to circumvent objects which can't be compared with equality against others
+ in a truth context, like with numpy arrays (#5606).
+ """
+ testdir.makepyfile(
+ dummy="""
+ class NumpyLike:
+ def __init__(self, value):
+ self.value = value
+ def __eq__(self, other):
+ raise ValueError("like numpy, cannot compare against others for truth")
+ FOO = NumpyLike(10)
+ """
+ )
+ testdir.makepyfile(
+ """
+ from unittest.mock import patch
+ import dummy
+ class Test(object):
+ @patch("dummy.FOO", new=dummy.NumpyLike(50))
+ def test_hello(self):
+ assert dummy.FOO.value == 50
+ """
+ )
+ reprec = testdir.inline_run()
+ reprec.assertoutcome(passed=1)
+
def test_mock(self, testdir):
pytest.importorskip("mock", "1.0.1")
testdir.makepyfile(
diff --git a/testing/test_config.py b/testing/test_config.py
index 143cb90d1..fc3659d2a 100644
--- a/testing/test_config.py
+++ b/testing/test_config.py
@@ -123,6 +123,12 @@ class TestParseIni:
config = testdir.parseconfigure(sub)
assert config.getini("minversion") == "2.0"
+ def test_ini_parse_error(self, testdir):
+ testdir.tmpdir.join("pytest.ini").write("addopts = -x")
+ result = testdir.runpytest()
+ assert result.ret != 0
+ result.stderr.fnmatch_lines(["ERROR: *pytest.ini:1: no section header defined"])
+
@pytest.mark.xfail(reason="probably not needed")
def test_confcutdir(self, testdir):
sub = testdir.mkdir("sub")
diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py
index 15643b081..3196f0ebd 100644
--- a/testing/test_junitxml.py
+++ b/testing/test_junitxml.py
@@ -592,7 +592,7 @@ class TestPython:
assert "hx" in fnode.toxml()
def test_assertion_binchars(self, testdir):
- """this test did fail when the escaping wasnt strict"""
+ """this test did fail when the escaping wasn't strict"""
testdir.makepyfile(
"""
@@ -715,7 +715,7 @@ def test_dont_configure_on_slaves(tmpdir):
return "pytest"
junitprefix = None
- # XXX: shouldnt need tmpdir ?
+ # XXX: shouldn't need tmpdir ?
xmlpath = str(tmpdir.join("junix.xml"))
register = gotten.append
diff --git a/testing/test_mark.py b/testing/test_mark.py
index 8747d1c6b..c8d5851ac 100644
--- a/testing/test_mark.py
+++ b/testing/test_mark.py
@@ -430,7 +430,7 @@ class TestFunctional:
def test_b(self):
assert True
class TestC(object):
- # this one didnt get marked
+ # this one didn't get marked
def test_d(self):
assert True
"""
diff --git a/testing/test_nose.py b/testing/test_nose.py
index f60c3af53..16d8d1fc0 100644
--- a/testing/test_nose.py
+++ b/testing/test_nose.py
@@ -253,7 +253,7 @@ def test_apiwrapper_problem_issue260(testdir):
def test_setup_teardown_linking_issue265(testdir):
- # we accidentally didnt integrate nose setupstate with normal setupstate
+ # we accidentally didn't integrate nose setupstate with normal setupstate
# this test ensures that won't happen again
testdir.makepyfile(
'''
diff --git a/testing/test_pytester.py b/testing/test_pytester.py
index f115ad3d0..cf92741af 100644
--- a/testing/test_pytester.py
+++ b/testing/test_pytester.py
@@ -230,8 +230,8 @@ class TestInlineRunModulesCleanup:
):
spy_factory = self.spy_factory()
monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory)
- original = dict(sys.modules)
testdir.syspathinsert()
+ original = dict(sys.modules)
testdir.makepyfile(import1="# you son of a silly person")
testdir.makepyfile(import2="# my hovercraft is full of eels")
test_mod = testdir.makepyfile(
diff --git a/testing/test_runner.py b/testing/test_runner.py
index 15180c071..82e413518 100644
--- a/testing/test_runner.py
+++ b/testing/test_runner.py
@@ -11,6 +11,7 @@ from _pytest import main
from _pytest import outcomes
from _pytest import reports
from _pytest import runner
+from _pytest.outcomes import OutcomeException
class TestSetupState:
@@ -990,3 +991,18 @@ class TestReportContents:
rep = reports[1]
assert rep.capstdout == ""
assert rep.capstderr == ""
+
+
+def test_outcome_exception_bad_msg():
+ """Check that OutcomeExceptions validate their input to prevent confusing errors (#5578)"""
+
+ def func():
+ pass
+
+ expected = (
+ "OutcomeException expected string as 'msg' parameter, got 'function' instead.\n"
+ "Perhaps you meant to use a mark?"
+ )
+ with pytest.raises(TypeError) as excinfo:
+ OutcomeException(func)
+ assert str(excinfo.value) == expected
diff --git a/testing/test_skipping.py b/testing/test_skipping.py
index 6bb5f7aff..8bba479f1 100644
--- a/testing/test_skipping.py
+++ b/testing/test_skipping.py
@@ -1066,7 +1066,8 @@ def test_module_level_skip_error(testdir):
testdir.makepyfile(
"""
import pytest
- @pytest.skip
+ pytest.skip("skip_module_level")
+
def test_func():
assert True
"""
diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py
index 591d67b6c..f61425b6b 100644
--- a/testing/test_stepwise.py
+++ b/testing/test_stepwise.py
@@ -207,7 +207,8 @@ def test_xfail_handling(testdir):
# because we are writing to the same file, mtime might not be affected enough to
# invalidate the cache, making this next run flaky
- testdir.tmpdir.join("__pycache__").remove()
+ if testdir.tmpdir.join("__pycache__").exists():
+ testdir.tmpdir.join("__pycache__").remove()
testdir.makepyfile(contents.format(assert_value="0", strict="True"))
result = testdir.runpytest("--sw", "-v")
result.stdout.fnmatch_lines(
diff --git a/testing/test_terminal.py b/testing/test_terminal.py
index bf029fbc5..381a5b2e1 100644
--- a/testing/test_terminal.py
+++ b/testing/test_terminal.py
@@ -233,7 +233,7 @@ class TestTerminal:
)
else:
result.stdout.fnmatch_lines(
- ["(to show a full traceback on KeyboardInterrupt use --fulltrace)"]
+ ["(to show a full traceback on KeyboardInterrupt use --full-trace)"]
)
result.stdout.fnmatch_lines(["*KeyboardInterrupt*"])
diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py
index 11556594b..ebde9044c 100644
--- a/testing/test_tmpdir.py
+++ b/testing/test_tmpdir.py
@@ -1,3 +1,5 @@
+import os
+import stat
import sys
import attr
@@ -303,22 +305,6 @@ class TestNumberedDir:
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_ignores_symlink(self, tmp_path):
the_symlink = tmp_path / (self.PREFIX + "current")
attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5"))
@@ -331,6 +317,83 @@ class TestNumberedDir:
assert folder.is_dir()
+class TestRmRf:
+ def test_rm_rf(self, tmp_path):
+ from _pytest.pathlib import rm_rf
+
+ adir = tmp_path / "adir"
+ adir.mkdir()
+ rm_rf(adir)
+
+ assert not adir.exists()
+
+ adir.mkdir()
+ afile = adir / "afile"
+ afile.write_bytes(b"aa")
+
+ rm_rf(adir)
+ assert not adir.exists()
+
+ def test_rm_rf_with_read_only_file(self, tmp_path):
+ """Ensure rm_rf can remove directories with read-only files in them (#5524)"""
+ from _pytest.pathlib import rm_rf
+
+ fn = tmp_path / "dir/foo.txt"
+ fn.parent.mkdir()
+
+ fn.touch()
+
+ self.chmod_r(fn)
+
+ rm_rf(fn.parent)
+
+ assert not fn.parent.is_dir()
+
+ def chmod_r(self, path):
+ mode = os.stat(str(path)).st_mode
+ os.chmod(str(path), mode & ~stat.S_IWRITE)
+
+ def test_rm_rf_with_read_only_directory(self, tmp_path):
+ """Ensure rm_rf can remove read-only directories (#5524)"""
+ from _pytest.pathlib import rm_rf
+
+ adir = tmp_path / "dir"
+ adir.mkdir()
+
+ (adir / "foo.txt").touch()
+ self.chmod_r(adir)
+
+ rm_rf(adir)
+
+ assert not adir.is_dir()
+
+ def test_on_rm_rf_error(self, tmp_path):
+ from _pytest.pathlib import on_rm_rf_error
+
+ adir = tmp_path / "dir"
+ adir.mkdir()
+
+ fn = adir / "foo.txt"
+ fn.touch()
+ self.chmod_r(fn)
+
+ # unknown exception
+ with pytest.warns(pytest.PytestWarning):
+ exc_info = (None, RuntimeError(), None)
+ on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path)
+ assert fn.is_file()
+
+ # unknown function
+ with pytest.warns(pytest.PytestWarning):
+ exc_info = (None, PermissionError(), None)
+ on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path)
+ assert fn.is_file()
+
+ exc_info = (None, PermissionError(), None)
+ on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path)
+ assert not fn.is_file()
+
+
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)."""
@@ -342,3 +405,24 @@ def attempt_symlink_to(path, to_path):
def test_tmpdir_equals_tmp_path(tmpdir, tmp_path):
assert Path(tmpdir) == tmp_path
+
+
+def test_basetemp_with_read_only_files(testdir):
+ """Integration test for #5524"""
+ testdir.makepyfile(
+ """
+ import os
+ import stat
+
+ def test(tmp_path):
+ fn = tmp_path / 'foo.txt'
+ fn.write_text('hello')
+ mode = os.stat(str(fn)).st_mode
+ os.chmod(str(fn), mode & ~stat.S_IREAD)
+ """
+ )
+ result = testdir.runpytest("--basetemp=tmp")
+ assert result.ret == 0
+ # running a second time and ensure we don't crash
+ result = testdir.runpytest("--basetemp=tmp")
+ assert result.ret == 0
diff --git a/testing/test_unittest.py b/testing/test_unittest.py
index 039068269..9b1b688ff 100644
--- a/testing/test_unittest.py
+++ b/testing/test_unittest.py
@@ -1048,3 +1048,39 @@ def test_setup_inheritance_skipping(testdir, test_name, expected_outcome):
testdir.copy_example("unittest/{}".format(test_name))
result = testdir.runpytest()
result.stdout.fnmatch_lines(["* {} in *".format(expected_outcome)])
+
+
+def test_BdbQuit(testdir):
+ testdir.makepyfile(
+ test_foo="""
+ import unittest
+
+ class MyTestCase(unittest.TestCase):
+ def test_bdbquit(self):
+ import bdb
+ raise bdb.BdbQuit()
+
+ def test_should_not_run(self):
+ pass
+ """
+ )
+ reprec = testdir.inline_run()
+ reprec.assertoutcome(failed=1, passed=1)
+
+
+def test_exit_outcome(testdir):
+ testdir.makepyfile(
+ test_foo="""
+ import pytest
+ import unittest
+
+ class MyTestCase(unittest.TestCase):
+ def test_exit_outcome(self):
+ pytest.exit("pytest_exit called")
+
+ def test_should_not_run(self):
+ pass
+ """
+ )
+ result = testdir.runpytest()
+ result.stdout.fnmatch_lines(["*Exit: pytest_exit called*", "*= no tests ran in *"])
diff --git a/tox.ini b/tox.ini
index 3e3afb3a3..e158ece67 100644
--- a/tox.ini
+++ b/tox.ini
@@ -127,11 +127,10 @@ norecursedirs = testing/example_scripts
xfail_strict=true
filterwarnings =
error
+ default:Using or importing the ABCs:DeprecationWarning:unittest2.*
ignore:Module already imported so cannot be rewritten:pytest.PytestWarning
- # produced by path.local
- ignore:bad escape.*:DeprecationWarning:re
- # produced by path.readlines
- ignore:.*U.*mode is deprecated:DeprecationWarning
+ # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8).
+ ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))
# produced by pytest-xdist
ignore:.*type argument to addoption.*:DeprecationWarning
# produced by python >=3.5 on execnet (pytest-xdist)
@@ -139,6 +138,8 @@ filterwarnings =
# pytest's own futurewarnings
ignore::pytest.PytestExperimentalApiWarning
# Do not cause SyntaxError for invalid escape sequences in py37.
+ # Those are caught/handled by pyupgrade, and not easy to filter with the
+ # module being the filename (with .py removed).
default:invalid escape sequence:DeprecationWarning
# ignore use of unregistered marks, because we use many to test the implementation
ignore::_pytest.warning_types.PytestUnknownMarkWarning