Merge branch 'master' into fix-flaky-test

This commit is contained in:
Bruno Oliveira 2020-06-02 11:33:15 -03:00 committed by GitHub
commit 7a8b8a5c9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 1579 additions and 1734 deletions

View File

@ -94,7 +94,7 @@ jobs:
os: ubuntu-latest
tox_env: "py38-xdist"
- name: "ubuntu-py39"
python: "3.8"
python: "3.9-dev"
os: ubuntu-latest
tox_env: "py39-xdist"
- name: "ubuntu-pypy3"
@ -128,18 +128,18 @@ jobs:
steps:
- uses: actions/checkout@v2
# For setuptools-scm.
- run: git fetch --prune --unshallow
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v2
if: matrix.python != '3.9-dev'
with:
python-version: ${{ matrix.python }}
- name: Set up Python ${{ matrix.python }} (deadsnakes)
uses: deadsnakes/action@v1.0.0
if: matrix.python == '3.9-dev'
with:
python-version: ${{ matrix.python }}
- name: install python3.9
if: matrix.tox_env == 'py39-xdist'
run: |
sudo add-apt-repository ppa:deadsnakes/nightly
sudo apt-get update
sudo apt-get install -y --no-install-recommends python3.9-dev python3.9-distutils
- name: Install dependencies
run: |
python -m pip install --upgrade pip
@ -177,8 +177,8 @@ jobs:
steps:
- uses: actions/checkout@v2
# For setuptools-scm.
- run: git fetch --prune --unshallow
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
with:

View File

@ -15,8 +15,8 @@ jobs:
steps:
- uses: actions/checkout@v2
# For setuptools-scm.
- run: git fetch --prune --unshallow
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2

View File

@ -63,6 +63,7 @@ Christian Tismer
Christoph Buelter
Christopher Dignam
Christopher Gilling
Claire Cecil
Claudio Madotto
CrazyMerlyn
Cyrus Maden
@ -102,11 +103,13 @@ Fabio Zadrozny
Felix Nieuwenhuizen
Feng Ma
Florian Bruhin
Florian Dahlitz
Floris Bruynooghe
Gabriel Reis
Gene Wood
George Kussumoto
Georgy Dyuldin
Gleb Nikonorov
Graham Horler
Greg Price
Gregory Lee
@ -275,6 +278,7 @@ Tom Dalton
Tom Viner
Tomáš Gavenčiak
Tomer Keren
Tor Colvin
Trevor Bekolay
Tyler Goodlet
Tzu-ping Chung

View File

@ -0,0 +1 @@
``TestReport.longrepr`` is now always an instance of ``ReprExceptionInfo``. Previously it was a ``str`` when a test failed with ``pytest.fail(..., pytrace=False)``.

View File

@ -0,0 +1,2 @@
Paths appearing in error messages are now correct in case the current working directory has
changed since the start of the session.

View File

@ -0,0 +1,10 @@
If an error is encountered while formatting the message in a logging call, for
example ``logging.warning("oh no!: %s: %s", "first")`` (a second argument is
missing), pytest now propagates the error, likely causing the test to fail.
Previously, such a mistake would cause an error to be printed to stderr, which
is not displayed by default for passing tests. This change makes the mistake
visible during testing.
You may supress this behavior temporarily or permanently by setting
``logging.raiseExceptions = False``.

View File

@ -0,0 +1 @@
Support deleting paths longer than 260 characters on windows created inside tmpdir.

View File

@ -0,0 +1,3 @@
A warning is now shown when an unknown key is read from a config INI file.
The `--strict-config` flag has been added to treat these warnings as errors.

View File

@ -0,0 +1 @@
Prevent pytest from printing ConftestImportFailure traceback to stdout.

View File

@ -0,0 +1,4 @@
When ``fd`` capturing is used, through ``--capture=fd`` or the ``capfd`` and
``capfdbinary`` fixtures, and the file descriptor (0, 1, 2) cannot be
duplicated, FD capturing is still performed. Previously, direct writes to the
file descriptors would fail or be lost in this case.

View File

@ -0,0 +1 @@
`pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins.

View File

@ -0,0 +1 @@
Prevent hiding the underlying exception when ``ConfTestImportFailure`` is raised.

1
changelog/7202.doc.rst Normal file
View File

@ -0,0 +1 @@
The development guide now links to the contributing section of the docs and 'RELEASING.rst' on GitHub.

View File

@ -0,0 +1,2 @@
Fix regression where running with ``--pdb`` would call the ``tearDown`` methods of ``unittest.TestCase``
subclasses for skipped tests.

1
changelog/7233.doc.rst Normal file
View File

@ -0,0 +1 @@
Add a note about ``--strict`` and ``--strict-markers`` and the preference for the latter one.

View File

@ -0,0 +1,3 @@
Introduced a new hook named `pytest_warning_recorded` to convey information about warnings captured by the internal `pytest` warnings plugin.
This hook is meant to replace `pytest_warning_captured`, which will be removed in a future release.

View File

@ -0,0 +1 @@
The dependency on the ``wcwidth`` package has been removed.

View File

@ -1 +1,6 @@
comment: off
# reference: https://docs.codecov.io/docs/codecovyml-reference
coverage:
status:
patch: true
project: false
comment: false

View File

@ -1,3 +0,0 @@
*.pyc
*.pyo
.DS_Store

View File

@ -1,37 +0,0 @@
Copyright (c) 2010 by Armin Ronacher.
Some rights reserved.
Redistribution and use in source and binary forms of the theme, with or
without modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
We kindly ask you to only use these themes in an unmodified manner just
for Flask and Flask-related products, not for unrelated projects. If you
like the visual style and want to use it for your own projects, please
consider making some larger changes to the themes (such as changing
font faces, sizes, colors or margins).
THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,31 +0,0 @@
Flask Sphinx Styles
===================
This repository contains sphinx styles for Flask and Flask related
projects. To use this style in your Sphinx documentation, follow
this guide:
1. put this folder as _themes into your docs folder. Alternatively
you can also use git submodules to check out the contents there.
2. add this to your conf.py:
sys.path.append(os.path.abspath('_themes'))
html_theme_path = ['_themes']
html_theme = 'flask'
The following themes exist:
- 'flask' - the standard flask documentation theme for large
projects
- 'flask_small' - small one-page theme. Intended to be used by
very small addon libraries for flask.
The following options exist for the flask_small theme:
[options]
index_logo = '' filename of a picture in _static
to be used as replacement for the
h1 in the index.rst file.
index_logo_height = 120px height of the index logo
github_fork = '' repository name on github for the
"fork me" badge

View File

@ -1,24 +0,0 @@
{%- extends "basic/layout.html" %}
{%- block extrahead %}
{{ super() }}
{% if theme_touch_icon %}
<link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" />
{% endif %}
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9">
{% endblock %}
{%- block relbar2 %}{% endblock %}
{% block header %}
{{ super() }}
{% if pagename == 'index' %}
<div class=indexwrapper>
{% endif %}
{% endblock %}
{%- block footer %}
<div class="footer">
&copy; Copyright {{ copyright }}.
Created using <a href="https://www.sphinx-doc.org/">Sphinx</a> {{ sphinx_version }}.
</div>
{% if pagename == 'index' %}
</div>
{% endif %}
{%- endblock %}

View File

@ -1,623 +0,0 @@
/*
* flasky.css_t
* ~~~~~~~~~~~~
*
* :copyright: Copyright 2010 by Armin Ronacher.
* :license: Flask Design License, see LICENSE for details.
*/
{% set page_width = '1020px' %}
{% set sidebar_width = '220px' %}
/* 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 = 'sans-serif' %}
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: {{ base_font }};
font-size: 16px;
background-color: white;
color: #000;
margin: 0;
padding: 0;
}
div.document {
width: {{ page_width }};
margin: 30px auto 0 auto;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 {{ sidebar_width }};
}
div.sphinxsidebar {
width: {{ sidebar_width }};
}
hr {
border: 0;
border-top: 1px solid #B1B4B6;
}
div.body {
background-color: #ffffff;
color: #3E4349;
padding: 0 30px 0 30px;
}
img.floatingflask {
padding: 0 0 10px 10px;
float: right;
}
div.footer {
width: {{ page_width }};
margin: 20px auto 30px auto;
font-size: 14px;
color: #888;
text-align: right;
}
div.footer a {
color: #888;
}
div.related {
display: none;
}
div.sphinxsidebar a {
text-decoration: none;
border-bottom: none;
}
div.sphinxsidebar a:hover {
color: {{ link_hover_color }};
border-bottom: 1px solid {{ link_hover_color }};
}
div.sphinxsidebar {
font-size: 14px;
line-height: 1.5;
}
div.sphinxsidebarwrapper {
padding: 18px 10px;
}
div.sphinxsidebarwrapper p.logo {
padding: 0 0 20px 0;
margin: 0;
text-align: center;
}
div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family: {{ header_font }};
color: #444;
font-size: 21px;
font-weight: normal;
margin: 16px 0 0 0;
padding: 0;
}
div.sphinxsidebar h4 {
font-size: 18px;
}
div.sphinxsidebar h3 a {
color: #444;
}
div.sphinxsidebar p.logo a,
div.sphinxsidebar h3 a,
div.sphinxsidebar p.logo a:hover,
div.sphinxsidebar h3 a:hover {
border: none;
}
div.sphinxsidebar p {
color: #555;
margin: 10px 0;
}
div.sphinxsidebar ul {
margin: 10px 0;
padding: 0;
color: #000;
}
div.sphinxsidebar input {
border: 1px solid #ccc;
font-family: {{ base_font }};
font-size: 1em;
}
/* -- body styles ----------------------------------------------------------- */
a {
color: {{ link_color }};
text-decoration: underline;
}
a:hover {
color: {{ link_hover_color }};
text-decoration: underline;
}
a.reference.internal em {
font-style: normal;
}
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: {{ header_font }};
font-weight: normal;
margin: 30px 0px 10px 0px;
padding: 0;
}
{% if theme_index_logo %}
div.indexwrapper h1 {
text-indent: -999999px;
background: url({{ theme_index_logo }}) no-repeat center center;
height: {{ theme_index_logo_height }};
}
{% else %}
div.indexwrapper div.body h1 {
font-size: 200%;
}
{% endif %}
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
div.body h2 { font-size: 180%; }
div.body h3 { font-size: 150%; }
div.body h4 { font-size: 130%; }
div.body h5 { font-size: 100%; }
div.body h6 { font-size: 100%; }
a.headerlink {
color: #ddd;
padding: 0 4px;
text-decoration: none;
}
a.headerlink:hover {
color: #444;
background: #eaeaea;
}
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;
padding: 10px 20px;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
div.admonition tt.xref, div.admonition a tt {
border-bottom: 1px solid #fafafa;
}
div.admonition p.admonition-title {
font-family: {{ header_font }};
font-weight: normal;
font-size: 24px;
margin: 0 0 10px 0;
padding: 0;
line-height: 1;
}
div.admonition :last-child {
margin-bottom: 0;
}
div.highlight {
background-color: white;
}
dt:target, .highlight {
background: #FAF3E8;
}
div.note, div.warning {
background-color: #eee;
border: 1px solid #ccc;
}
div.seealso {
background-color: #ffc;
border: 1px solid #ff6;
}
div.topic {
background-color: #eee;
}
div.topic a {
text-decoration: none;
border-bottom: none;
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre, tt, code {
font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
font-size: 0.9em;
background: #eee;
}
img.screenshot {
}
tt.descname, tt.descclassname {
font-size: 0.95em;
}
tt.descname {
padding-right: 0.08em;
}
img.screenshot {
-moz-box-shadow: 2px 2px 4px #eee;
-webkit-box-shadow: 2px 2px 4px #eee;
box-shadow: 2px 2px 4px #eee;
}
table.docutils {
border: 1px solid #888;
-moz-box-shadow: 2px 2px 4px #eee;
-webkit-box-shadow: 2px 2px 4px #eee;
box-shadow: 2px 2px 4px #eee;
}
table.docutils td, table.docutils th {
border: 1px solid #888;
padding: 0.25em 0.7em;
}
table.field-list, table.footnote {
border: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
table.footnote {
margin: 15px 0;
width: 100%;
border: 1px solid #eee;
background: #fdfdfd;
font-size: 0.9em;
}
table.footnote + table.footnote {
margin-top: -15px;
border-top: none;
}
table.field-list th {
padding: 0 0.8em 0 0;
}
table.field-list td {
padding: 0;
}
table.footnote td.label {
width: 0px;
padding: 0.3em 0 0.3em 0.5em;
}
table.footnote td {
padding: 0.3em 0.5em;
}
dl {
margin: 0;
padding: 0;
}
dl dd {
margin-left: 30px;
}
blockquote {
margin: 0 0 0 30px;
padding: 0;
}
ul, ol {
margin: 10px 0 10px 30px;
padding: 0;
}
pre {
background: #eee;
padding: 7px 12px;
line-height: 1.3em;
}
tt {
background-color: #ecf0f3;
color: #222;
/* padding: 1px 2px; */
}
tt.xref, a tt {
background-color: #FBFBFB;
border-bottom: 1px solid white;
}
a.reference {
text-decoration: none;
border-bottom: 1px dotted {{ link_color }};
}
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;
vertical-align: top;
border-bottom: 1px dotted {{ link_color }};
}
a.footnote-reference:hover {
border-bottom: 1px solid {{ link_hover_color }};
}
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) {
div.sphinxsidebar {
display: none;
}
div.document {
width: 100%;
}
div.documentwrapper {
margin-left: 0;
margin-top: 0;
margin-right: 0;
margin-bottom: 0;
}
div.bodywrapper {
margin-top: 0;
margin-right: 0;
margin-bottom: 0;
margin-left: 0;
}
ul {
margin-left: 0;
}
.document {
width: auto;
}
.footer {
width: auto;
}
.bodywrapper {
margin: 0;
}
.footer {
width: auto;
}
.github {
display: none;
}
}
@media screen and (max-width: 875px) {
body {
margin: 0;
padding: 20px 30px;
}
div.documentwrapper {
float: none;
background: white;
}
div.sphinxsidebar {
display: block;
float: none;
width: 102.5%;
margin: 50px -30px -20px -30px;
padding: 10px 20px;
background: #333;
color: white;
}
div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
div.sphinxsidebar h3 a, div.sphinxsidebar ul {
color: white;
}
div.sphinxsidebar a {
color: #aaa;
}
div.sphinxsidebar p.logo {
display: none;
}
div.document {
width: 100%;
margin: 0;
}
div.related {
display: block;
margin: 0;
padding: 10px 0 20px 0;
}
div.related ul,
div.related ul li {
margin: 0;
padding: 0;
}
div.footer {
display: none;
}
div.bodywrapper {
margin: 0;
}
div.body {
min-height: 0;
padding: 0;
}
.rtd_doc_footer {
display: none;
}
.document {
width: auto;
}
.footer {
width: auto;
}
.footer {
width: auto;
}
.github {
display: none;
}
}
/* misc. */
.revsys-inline {
display: none!important;
}

View File

@ -1,9 +0,0 @@
[theme]
inherit = basic
stylesheet = flasky.css
pygments_style = flask_theme_support.FlaskyStyle
[options]
index_logo = ''
index_logo_height = 120px
touch_icon =

View File

@ -1,87 +0,0 @@
# flasky extensions. flasky pygments style based on tango style
from pygments.style import Style
from pygments.token import Comment
from pygments.token import Error
from pygments.token import Generic
from pygments.token import Keyword
from pygments.token import Literal
from pygments.token import Name
from pygments.token import Number
from pygments.token import Operator
from pygments.token import Other
from pygments.token import Punctuation
from pygments.token import String
from pygments.token import Whitespace
class FlaskyStyle(Style):
background_color = "#f8f8f8"
default_style = ""
styles = {
# No corresponding class for the following:
# Text: "", # class: ''
Whitespace: "underline #f8f8f8", # class: 'w'
Error: "#a40000 border:#ef2929", # class: 'err'
Other: "#000000", # class 'x'
Comment: "italic #8f5902", # class: 'c'
Comment.Preproc: "noitalic", # class: 'cp'
Keyword: "bold #004461", # class: 'k'
Keyword.Constant: "bold #004461", # class: 'kc'
Keyword.Declaration: "bold #004461", # class: 'kd'
Keyword.Namespace: "bold #004461", # class: 'kn'
Keyword.Pseudo: "bold #004461", # class: 'kp'
Keyword.Reserved: "bold #004461", # class: 'kr'
Keyword.Type: "bold #004461", # class: 'kt'
Operator: "#582800", # class: 'o'
Operator.Word: "bold #004461", # class: 'ow' - like keywords
Punctuation: "bold #000000", # class: 'p'
# because special names such as Name.Class, Name.Function, etc.
# are not recognized as such later in the parsing, we choose them
# to look the same as ordinary variables.
Name: "#000000", # class: 'n'
Name.Attribute: "#c4a000", # class: 'na' - to be revised
Name.Builtin: "#004461", # class: 'nb'
Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
Name.Class: "#000000", # class: 'nc' - to be revised
Name.Constant: "#000000", # class: 'no' - to be revised
Name.Decorator: "#888", # class: 'nd' - to be revised
Name.Entity: "#ce5c00", # class: 'ni'
Name.Exception: "bold #cc0000", # class: 'ne'
Name.Function: "#000000", # class: 'nf'
Name.Property: "#000000", # class: 'py'
Name.Label: "#f57900", # class: 'nl'
Name.Namespace: "#000000", # class: 'nn' - to be revised
Name.Other: "#000000", # class: 'nx'
Name.Tag: "bold #004461", # class: 'nt' - like a keyword
Name.Variable: "#000000", # class: 'nv' - to be revised
Name.Variable.Class: "#000000", # class: 'vc' - to be revised
Name.Variable.Global: "#000000", # class: 'vg' - to be revised
Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
Number: "#990000", # class: 'm'
Literal: "#000000", # class: 'l'
Literal.Date: "#000000", # class: 'ld'
String: "#4e9a06", # class: 's'
String.Backtick: "#4e9a06", # class: 'sb'
String.Char: "#4e9a06", # class: 'sc'
String.Doc: "italic #8f5902", # class: 'sd' - like a comment
String.Double: "#4e9a06", # class: 's2'
String.Escape: "#4e9a06", # class: 'se'
String.Heredoc: "#4e9a06", # class: 'sh'
String.Interpol: "#4e9a06", # class: 'si'
String.Other: "#4e9a06", # class: 'sx'
String.Regex: "#4e9a06", # class: 'sr'
String.Single: "#4e9a06", # class: 's1'
String.Symbol: "#4e9a06", # class: 'ss'
Generic: "#000000", # class: 'g'
Generic.Deleted: "#a40000", # class: 'gd'
Generic.Emph: "italic #000000", # class: 'ge'
Generic.Error: "#ef2929", # class: 'gr'
Generic.Heading: "bold #000080", # class: 'gh'
Generic.Inserted: "#00A000", # class: 'gi'
Generic.Output: "#888", # class: 'go'
Generic.Prompt: "#745334", # class: 'gp'
Generic.Strong: "bold #000000", # class: 'gs'
Generic.Subheading: "bold #800080", # class: 'gu'
Generic.Traceback: "bold #a40000", # class: 'gt'
}

View File

@ -145,8 +145,7 @@ The return value from ``readouterr`` changed to a ``namedtuple`` with two attrib
If the code under test writes non-textual data, you can capture this using
the ``capsysbinary`` fixture which instead returns ``bytes`` from
the ``readouterr`` method. The ``capfsysbinary`` fixture is currently only
available in python 3.
the ``readouterr`` method.

View File

@ -43,6 +43,7 @@ todo_include_todos = 1
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
"pallets_sphinx_themes",
"pygments_pytest",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
@ -142,7 +143,7 @@ html_theme = "flask"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {"index_logo": None}
# html_theme_options = {"index_logo": None}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []

View File

@ -2,59 +2,6 @@
Development Guide
=================
Some general guidelines regarding development in pytest for maintainers and contributors. Nothing here
is set in stone and can't be changed, feel free to suggest improvements or changes in the workflow.
Code Style
----------
* `PEP-8 <https://www.python.org/dev/peps/pep-0008>`_
* `flake8 <https://pypi.org/project/flake8/>`_ for quality checks
* `invoke <http://www.pyinvoke.org/>`_ to automate development tasks
Branches
--------
We have two long term branches:
* ``master``: contains the code for the next bug-fix release.
* ``features``: contains the code with new features for the next minor release.
The official repository usually does not contain topic branches, developers and contributors should create topic
branches in their own forks.
Exceptions can be made for cases where more than one contributor is working on the same
topic or where it makes sense to use some automatic capability of the main repository, such as automatic docs from
`readthedocs <readthedocs.org>`_ for a branch dealing with documentation refactoring.
Issues
------
Any question, feature, bug or proposal is welcome as an issue. Users are encouraged to use them whenever they need.
GitHub issues should use labels to categorize them. Labels should be created sporadically, to fill a niche; we should
avoid creating labels just for the sake of creating them.
Each label should include a description in the GitHub's interface stating its purpose.
Labels are managed using `labels <https://github.com/hackebrot/labels>`_. All the labels in the repository
are kept in ``.github/labels.toml``, so any changes should be via PRs to that file.
After a PR is accepted and merged, one of the maintainers must manually synchronize the labels file with the
GitHub repository.
Temporary labels
~~~~~~~~~~~~~~~~
To classify issues for a special event it is encouraged to create a temporary label. This helps those involved to find
the relevant issues to work on. Examples of that are sprints in Python events or global hacking events.
* ``temporary: EP2017 sprint``: candidate issues or PRs tackled during the EuroPython 2017
Issues created at those events should have other relevant labels added as well.
Those labels should be removed after they are no longer relevant.
.. include:: ../../RELEASING.rst
The contributing guidelines are to be found :ref:`here <contributing>`.
The release procedure for pytest is documented on
`GitHub <https://github.com/pytest-dev/pytest/blob/master/RELEASING.rst>`_.

View File

@ -153,6 +153,57 @@ Once you develop multiple tests, you may want to group them into a class. pytest
The first test passed and the second failed. You can easily see the intermediate values in the assertion to help you understand the reason for the failure.
Grouping tests in classes can be beneficial for the following reasons:
* Test organization
* Sharing fixtures for tests only in that particular class
* Applying marks at the class level and having them implicitly apply to all tests
Something to be aware of when grouping tests inside classes is that each test has a unique instance of the class.
Having each test share the same class instance would be very detrimental to test isolation and would promote poor test practices.
This is outlined below:
.. code-block:: python
class TestClassDemoInstance:
def test_one(self):
assert 0
def test_two(self):
assert 0
.. code-block:: pytest
$ pytest -k TestClassDemoInstance -q
FF [100%]
================================== FAILURES ===================================
_______________________ TestClassDemoInstance.test_one ________________________
self = <test_example.TestClassDemoInstance object at 0x0000019BBB9EEDA0>
request = <FixtureRequest for <Function test_one>>
def test_one(self, request):
> assert 0
E assert 0
testing\test_example.py:4: AssertionError
_______________________ TestClassDemoInstance.test_two ________________________
self = <test_example.TestClassDemoInstance object at 0x0000019BBB9F3D68>
request = <FixtureRequest for <Function test_two>>
def test_two(self, request):
> assert 0
E assert 0
testing\test_example.py:7: AssertionError
=========================== short test summary info ===========================
FAILED testing/test_example.py::TestClassDemoInstance::test_one - assert 0
FAILED testing/test_example.py::TestClassDemoInstance::test_two - assert 0
2 failed in 0.11s
Request a unique temporary directory for functional tests
--------------------------------------------------------------

View File

@ -711,6 +711,7 @@ Session related reporting hooks:
.. autofunction:: pytest_fixture_setup
.. autofunction:: pytest_fixture_post_finalizer
.. autofunction:: pytest_warning_captured
.. autofunction:: pytest_warning_recorded
Central hook for reporting about test execution:
@ -1447,6 +1448,11 @@ passed multiple times. The expected format is ``name=value``. For example::
slow
serial
.. note::
The use of ``--strict-markers`` is highly preferred. ``--strict`` was kept for
backward compatibility only and may be confusing for others as it only applies to
markers and not to other options.
.. confval:: minversion
Specifies a minimal pytest version required for running tests.

View File

@ -1,4 +1,5 @@
pallets-sphinx-themes
pygments-pytest>=1.1.0
sphinx-removed-in>=0.2.0
sphinx>=1.8.2,<2.1
sphinxcontrib-trio
sphinx-removed-in>=0.2.0

View File

@ -0,0 +1,15 @@
import sys
from subprocess import call
def main():
"""
Platform agnostic wrapper script for towncrier.
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
"""
with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file:
return call(("towncrier", "--draft"), stdout=draft_file)
if __name__ == "__main__":
sys.exit(main())

View File

@ -12,7 +12,6 @@ INSTALL_REQUIRES = [
'colorama;sys_platform=="win32"',
"pluggy>=0.12,<1.0",
'importlib-metadata>=0.12;python_version<"3.8"',
"wcwidth",
]

View File

@ -46,7 +46,7 @@ if TYPE_CHECKING:
from typing_extensions import Literal
from weakref import ReferenceType
_TracebackStyle = Literal["long", "short", "line", "no", "native"]
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value"]
class Code:
@ -268,10 +268,6 @@ class TracebackEntry:
return tbh
def __str__(self) -> str:
try:
fn = str(self.path)
except py.error.Error:
fn = "???"
name = self.frame.code.name
try:
line = str(self.statement).lstrip()
@ -279,7 +275,7 @@ class TracebackEntry:
raise
except BaseException:
line = "???"
return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line)
return " File %r:%d in %s\n %s\n" % (self.path, self.lineno + 1, name, line)
@property
def name(self) -> str:
@ -587,7 +583,7 @@ class ExceptionInfo(Generic[_E]):
Show locals per traceback entry.
Ignored if ``style=="native"``.
:param str style: long|short|no|native traceback style
:param str style: long|short|no|native|value traceback style
:param bool abspath:
If paths should be changed to absolute or left unchanged.
@ -762,16 +758,15 @@ class FormattedExcinfo:
def repr_traceback_entry(
self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None
) -> "ReprEntry":
source = self._getentrysource(entry)
if source is None:
source = Source("???")
line_index = 0
else:
line_index = entry.lineno - entry.getfirstlinesource()
lines = [] # type: List[str]
style = entry._repr_style if entry._repr_style is not None else self.style
if style in ("short", "long"):
source = self._getentrysource(entry)
if source is None:
source = Source("???")
line_index = 0
else:
line_index = entry.lineno - entry.getfirstlinesource()
short = style == "short"
reprargs = self.repr_args(entry) if not short else None
s = self.get_source(source, line_index, excinfo, short=short)
@ -784,9 +779,14 @@ class FormattedExcinfo:
reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
localsrepr = self.repr_locals(entry.locals)
return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
if excinfo:
lines.extend(self.get_exconly(excinfo, indent=4))
return ReprEntry(lines, None, None, None, style)
elif style == "value":
if excinfo:
lines.extend(str(excinfo.value).split("\n"))
return ReprEntry(lines, None, None, None, style)
else:
if excinfo:
lines.extend(self.get_exconly(excinfo, indent=4))
return ReprEntry(lines, None, None, None, style)
def _makepath(self, path):
if not self.abspath:
@ -810,6 +810,11 @@ class FormattedExcinfo:
last = traceback[-1]
entries = []
if self.style == "value":
reprentry = self.repr_traceback_entry(last, excinfo)
entries.append(reprentry)
return ReprTraceback(entries, None, style=self.style)
for index, entry in enumerate(traceback):
einfo = (last == entry) and excinfo or None
reprentry = self.repr_traceback_entry(entry, einfo)
@ -869,7 +874,9 @@ class FormattedExcinfo:
seen.add(id(e))
if excinfo_:
reprtraceback = self.repr_traceback(excinfo_)
reprcrash = excinfo_._getreprcrash() # type: Optional[ReprFileLocation]
reprcrash = (
excinfo_._getreprcrash() if self.style != "value" else None
) # type: Optional[ReprFileLocation]
else:
# fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work
@ -1052,8 +1059,11 @@ class ReprEntry(TerminalRepr):
"Unexpected failure lines between source lines:\n"
+ "\n".join(self.lines)
)
indents.append(line[:indent_size])
source_lines.append(line[indent_size:])
if self.style == "value":
source_lines.append(line)
else:
indents.append(line[:indent_size])
source_lines.append(line[indent_size:])
else:
seeing_failures = True
failure_lines.append(line)

View File

@ -2,12 +2,12 @@
import os
import shutil
import sys
import unicodedata
from functools import lru_cache
from typing import Optional
from typing import Sequence
from typing import TextIO
from .wcwidth import wcswidth
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
@ -22,17 +22,6 @@ def get_terminal_width() -> int:
return width
@lru_cache(100)
def char_width(c: str) -> int:
# Fullwidth and Wide -> 2, all else (including Ambiguous) -> 1.
return 2 if unicodedata.east_asian_width(c) in ("F", "W") else 1
def get_line_width(text: str) -> int:
text = unicodedata.normalize("NFC", text)
return sum(char_width(c) for c in text)
def should_do_markup(file: TextIO) -> bool:
if os.environ.get("PY_COLORS") == "1":
return True
@ -99,7 +88,7 @@ class TerminalWriter:
@property
def width_of_current_line(self) -> int:
"""Return an estimate of the width so far in the current line."""
return get_line_width(self._current_line)
return wcswidth(self._current_line)
def markup(self, text: str, **markup: bool) -> str:
for name in markup:

View File

@ -0,0 +1,55 @@
import unicodedata
from functools import lru_cache
@lru_cache(100)
def wcwidth(c: str) -> int:
"""Determine how many columns are needed to display a character in a terminal.
Returns -1 if the character is not printable.
Returns 0, 1 or 2 for other characters.
"""
o = ord(c)
# ASCII fast path.
if 0x20 <= o < 0x07F:
return 1
# Some Cf/Zp/Zl characters which should be zero-width.
if (
o == 0x0000
or 0x200B <= o <= 0x200F
or 0x2028 <= o <= 0x202E
or 0x2060 <= o <= 0x2063
):
return 0
category = unicodedata.category(c)
# Control characters.
if category == "Cc":
return -1
# Combining characters with zero width.
if category in ("Me", "Mn"):
return 0
# Full/Wide east asian characters.
if unicodedata.east_asian_width(c) in ("F", "W"):
return 2
return 1
def wcswidth(s: str) -> int:
"""Determine how many columns are needed to display a string in a terminal.
Returns -1 if the string contains non-printable characters.
"""
width = 0
for c in unicodedata.normalize("NFC", s):
wc = wcwidth(c)
if wc < 0:
return -1
width += wc
return width

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,6 @@ from typing import Union
import attr
import py
from packaging.version import Version
from pluggy import HookimplMarker
from pluggy import HookspecMarker
from pluggy import PluginManager
@ -1031,6 +1030,7 @@ class Config:
self.known_args_namespace = ns = self._parser.parse_known_args(
args, namespace=copy.copy(self.option)
)
self._validatekeys()
if self.known_args_namespace.confcutdir is None and self.inifile:
confcutdir = py.path.local(self.inifile).dirname
self.known_args_namespace.confcutdir = confcutdir
@ -1059,6 +1059,9 @@ class Config:
minver = self.inicfg.get("minversion", None)
if minver:
# Imported lazily to improve start-up time.
from packaging.version import Version
if Version(minver) > Version(pytest.__version__):
raise pytest.UsageError(
"%s:%d: requires pytest-%s, actual pytest-%s'"
@ -1070,6 +1073,17 @@ class Config:
)
)
def _validatekeys(self):
for key in self._get_unknown_ini_keys():
message = "Unknown config ini key: {}\n".format(key)
if self.known_args_namespace.strict_config:
fail(message, pytrace=False)
sys.stderr.write("WARNING: {}".format(message))
def _get_unknown_ini_keys(self) -> List[str]:
parser_inicfg = self._parser._inidict
return [name for name in self.inicfg if name not in parser_inicfg]
def parse(self, args: List[str], addopts: bool = True) -> None:
# parse given cmdline arguments into this config object.
assert not hasattr(

View File

@ -4,6 +4,7 @@ import functools
import sys
from _pytest import outcomes
from _pytest.config import ConftestImportFailure
from _pytest.config import hookimpl
from _pytest.config.exceptions import UsageError
@ -338,6 +339,10 @@ def _postmortem_traceback(excinfo):
# A doctest.UnexpectedException is not useful for post_mortem.
# Use the underlying exception instead:
return excinfo.value.exc_info[2]
elif isinstance(excinfo.value, ConftestImportFailure):
# A config.ConftestImportFailure is not useful for post_mortem.
# Use the underlying exception instead:
return excinfo.value.excinfo[2]
else:
return excinfo._excinfo[2]

View File

@ -80,3 +80,8 @@ MINUS_K_COLON = PytestDeprecationWarning(
"The `-k 'expr:'` syntax to -k is deprecated.\n"
"Please open an issue if you use this and want a replacement."
)
WARNING_CAPTURED_HOOK = PytestDeprecationWarning(
"The pytest_warning_captured is deprecated and will be removed in a future release.\n"
"Please use pytest_warning_recorded instead."
)

View File

@ -41,8 +41,11 @@ def pytest_addoption(parser):
group.addoption(
"--version",
"-V",
action="store_true",
help="display pytest version and information about plugins.",
action="count",
default=0,
dest="version",
help="display pytest version and information about plugins."
"When given twice, also display information about plugins.",
)
group._addoption(
"-h",
@ -116,19 +119,22 @@ def pytest_cmdline_parse():
def showversion(config):
sys.stderr.write(
"This is pytest version {}, imported from {}\n".format(
pytest.__version__, pytest.__file__
if config.option.version > 1:
sys.stderr.write(
"This is pytest version {}, imported from {}\n".format(
pytest.__version__, pytest.__file__
)
)
)
plugininfo = getpluginversioninfo(config)
if plugininfo:
for line in plugininfo:
sys.stderr.write(line + "\n")
plugininfo = getpluginversioninfo(config)
if plugininfo:
for line in plugininfo:
sys.stderr.write(line + "\n")
else:
sys.stderr.write("pytest {}\n".format(pytest.__version__))
def pytest_cmdline_main(config):
if config.option.version:
if config.option.version > 0:
showversion(config)
return 0
elif config.option.help:

View File

@ -8,9 +8,11 @@ from typing import Union
from pluggy import HookspecMarker
from .deprecated import COLLECT_DIRECTORY_HOOK
from .deprecated import WARNING_CAPTURED_HOOK
from _pytest.compat import TYPE_CHECKING
if TYPE_CHECKING:
import warnings
from _pytest.config import Config
from _pytest.main import Session
from _pytest.reports import BaseReport
@ -620,8 +622,40 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""
@hookspec(historic=True)
@hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK)
def pytest_warning_captured(warning_message, when, item, location):
"""(**Deprecated**) Process a warning captured by the internal pytest warnings plugin.
This hook is considered deprecated and will be removed in a future pytest version.
Use :func:`pytest_warning_recorded` instead.
:param warnings.WarningMessage warning_message:
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
the same attributes as the parameters of :py:func:`warnings.showwarning`.
:param str when:
Indicates when the warning was captured. Possible values:
* ``"config"``: during pytest configuration/initialization stage.
* ``"collect"``: during test collection.
* ``"runtest"``: during test execution.
:param pytest.Item|None item:
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
:param tuple location:
Holds information about the execution context of the captured warning (filename, linenumber, function).
``function`` evaluates to <module> when the execution context is at the module level.
"""
@hookspec(historic=True)
def pytest_warning_recorded(
warning_message: "warnings.WarningMessage",
when: str,
nodeid: str,
location: Tuple[str, int, str],
):
"""
Process a warning captured by the internal pytest warnings plugin.
@ -636,11 +670,7 @@ def pytest_warning_captured(warning_message, when, item, location):
* ``"collect"``: during test collection.
* ``"runtest"``: during test execution.
:param pytest.Item|None item:
**DEPRECATED**: This parameter is incompatible with ``pytest-xdist``, and will always receive ``None``
in a future release.
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
:param str nodeid: full id of the item
:param tuple location:
Holds information about the execution context of the captured warning (filename, linenumber, function).

View File

@ -202,10 +202,8 @@ class _NodeReporter:
if hasattr(report, "wasxfail"):
self._add_simple(Junit.skipped, "xfail-marked test passes unexpectedly")
else:
if hasattr(report.longrepr, "reprcrash"):
if getattr(report.longrepr, "reprcrash", None) is not None:
message = report.longrepr.reprcrash.message
elif isinstance(report.longrepr, str):
message = report.longrepr
else:
message = str(report.longrepr)
message = bin_xml_escape(message)

View File

@ -312,6 +312,14 @@ class LogCaptureHandler(logging.StreamHandler):
self.records = []
self.stream = StringIO()
def handleError(self, record: logging.LogRecord) -> None:
if logging.raiseExceptions:
# Fail the test if the log message is bad (emit failed).
# The default behavior of logging is to print "Logging error"
# to stderr with the call stack and some extra details.
# pytest wants to make such mistakes visible during testing.
raise
class LogCaptureFixture:
"""Provides access and control of log capturing."""
@ -499,9 +507,7 @@ class LoggingPlugin:
# File logging.
self.log_file_level = get_log_level_for_setting(config, "log_file_level")
log_file = get_option_ini(config, "log_file") or os.devnull
self.log_file_handler = logging.FileHandler(
log_file, mode="w", encoding="UTF-8"
)
self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
log_file_format = get_option_ini(config, "log_file_format", "log_format")
log_file_date_format = get_option_ini(
config, "log_file_date_format", "log_date_format"
@ -687,6 +693,16 @@ class LoggingPlugin:
self.log_file_handler.close()
class _FileHandler(logging.FileHandler):
"""
Custom FileHandler with pytest tweaks.
"""
def handleError(self, record: logging.LogRecord) -> None:
# Handled by LogCaptureHandler.
pass
class _LiveLoggingStreamHandler(logging.StreamHandler):
"""
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
@ -737,6 +753,10 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
self._section_name_shown = True
super().emit(record)
def handleError(self, record: logging.LogRecord) -> None:
# Handled by LogCaptureHandler.
pass
class _LiveLoggingNullHandler(logging.NullHandler):
"""A handler used when live logging is disabled."""
@ -746,3 +766,7 @@ class _LiveLoggingNullHandler(logging.NullHandler):
def set_when(self, when):
pass
def handleError(self, record: logging.LogRecord) -> None:
# Handled by LogCaptureHandler.
pass

View File

@ -70,6 +70,11 @@ def pytest_addoption(parser):
default=0,
help="exit after first num failures or errors.",
)
group._addoption(
"--strict-config",
action="store_true",
help="invalid ini keys for the `pytest` section of the configuration file raise errors.",
)
group._addoption(
"--strict-markers",
"--strict",

View File

@ -19,6 +19,7 @@ from _pytest._code.code import ReprExceptionInfo
from _pytest.compat import cached_property
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.config import PytestPluginManager
from _pytest.deprecated import NODE_USE_FROM_PARENT
from _pytest.fixtures import FixtureDef
@ -28,7 +29,7 @@ from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords
from _pytest.outcomes import fail
from _pytest.outcomes import Failed
from _pytest.pathlib import Path
from _pytest.store import Store
if TYPE_CHECKING:
@ -331,11 +332,13 @@ class Node(metaclass=NodeMeta):
pass
def _repr_failure_py(
self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]], style=None
self, excinfo: ExceptionInfo[BaseException], style=None,
) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]:
if isinstance(excinfo.value, ConftestImportFailure):
excinfo = ExceptionInfo(excinfo.value.excinfo)
if isinstance(excinfo.value, fail.Exception):
if not excinfo.value.pytrace:
return str(excinfo.value)
style = "value"
if isinstance(excinfo.value, FixtureLookupError):
return excinfo.value.formatrepr()
if self.config.getoption("fulltrace", False):
@ -359,9 +362,14 @@ class Node(metaclass=NodeMeta):
else:
truncate_locals = True
# excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
# It is possible for a fixture/test to change the CWD while this code runs, which
# would then result in the user seeing confusing paths in the failure message.
# To fix this, if the CWD changed, always display the full absolute path.
# It will be better to just always display paths relative to invocation_dir, but
# this requires a lot of plumbing (#6428).
try:
os.getcwd()
abspath = False
abspath = Path(os.getcwd()) != Path(self.config.invocation_dir)
except OSError:
abspath = True
@ -456,10 +464,7 @@ def _check_initialpaths_for_relpath(session, fspath):
class FSHookProxy:
def __init__(
self, fspath: py.path.local, pm: PytestPluginManager, remove_mods
) -> None:
self.fspath = fspath
def __init__(self, pm: PytestPluginManager, remove_mods) -> None:
self.pm = pm
self.remove_mods = remove_mods
@ -510,7 +515,7 @@ class FSCollector(Collector):
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
if remove_mods:
# one or more conftests are not in use at this fspath
proxy = FSHookProxy(fspath, pm, remove_mods)
proxy = FSHookProxy(pm, remove_mods)
else:
# all plugins are active for this fspath
proxy = self.config.hook

View File

@ -9,8 +9,6 @@ from typing import cast
from typing import Optional
from typing import TypeVar
from packaging.version import Version
TYPE_CHECKING = False # avoid circular import through compat
if TYPE_CHECKING:
@ -217,6 +215,9 @@ def importorskip(
return mod
verattr = getattr(mod, "__version__", None)
if minversion is not None:
# Imported lazily to improve start-up time.
from packaging.version import Version
if verattr is None or Version(verattr) < Version(minversion):
raise Skipped(
"module %r has __version__ %r, required is: %r"

View File

@ -100,10 +100,41 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
return True
def ensure_extended_length_path(path: Path) -> Path:
"""Get the extended-length version of a path (Windows).
On Windows, by default, the maximum length of a path (MAX_PATH) is 260
characters, and operations on paths longer than that fail. But it is possible
to overcome this by converting the path to "extended-length" form before
performing the operation:
https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
On Windows, this function returns the extended-length absolute version of path.
On other platforms it returns path unchanged.
"""
if sys.platform.startswith("win32"):
path = path.resolve()
path = Path(get_extended_length_path_str(str(path)))
return path
def get_extended_length_path_str(path: str) -> str:
"""Converts to extended length path as a str"""
long_path_prefix = "\\\\?\\"
unc_long_path_prefix = "\\\\?\\UNC\\"
if path.startswith((long_path_prefix, unc_long_path_prefix)):
return path
# UNC
if path.startswith("\\\\"):
return unc_long_path_prefix + path[2:]
return long_path_prefix + path
def rm_rf(path: Path) -> None:
"""Remove the path contents recursively, even if some elements
are read-only.
"""
path = ensure_extended_length_path(path)
onerror = partial(on_rm_rf_error, start_path=path)
shutil.rmtree(str(path), onerror=onerror)
@ -220,6 +251,7 @@ def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
def maybe_delete_a_numbered_dir(path: Path) -> None:
"""removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
path = ensure_extended_length_path(path)
lock_path = None
try:
lock_path = create_cleanup_lock(path)

View File

@ -25,8 +25,7 @@ import py
import pytest
from _pytest import timing
from _pytest._code import Source
from _pytest.capture import MultiCapture
from _pytest.capture import SysCapture
from _pytest.capture import _get_multicapture
from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
@ -687,11 +686,41 @@ class Testdir:
return py.iniconfig.IniConfig(p)["pytest"]
def makepyfile(self, *args, **kwargs):
"""Shortcut for .makefile() with a .py extension."""
r"""Shortcut for .makefile() with a .py extension.
Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
existing files.
Examples:
.. code-block:: python
def test_something(testdir):
# initial file is created test_something.py
testdir.makepyfile("foobar")
# to create multiple files, pass kwargs accordingly
testdir.makepyfile(custom="foobar")
# at this point, both 'test_something.py' & 'custom.py' exist in the test directory
"""
return self._makefile(".py", args, kwargs)
def maketxtfile(self, *args, **kwargs):
"""Shortcut for .makefile() with a .txt extension."""
r"""Shortcut for .makefile() with a .txt extension.
Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
existing files.
Examples:
.. code-block:: python
def test_something(testdir):
# initial file is created test_something.txt
testdir.maketxtfile("foobar")
# to create multiple files, pass kwargs accordingly
testdir.maketxtfile(custom="foobar")
# at this point, both 'test_something.txt' & 'custom.txt' exist in the test directory
"""
return self._makefile(".txt", args, kwargs)
def syspathinsert(self, path=None):
@ -942,7 +971,7 @@ class Testdir:
if syspathinsert:
self.syspathinsert()
now = timing.time()
capture = MultiCapture(Capture=SysCapture)
capture = _get_multicapture("sys")
capture.start_capturing()
try:
try:

View File

@ -27,6 +27,7 @@ import pytest
from _pytest import nodes
from _pytest import timing
from _pytest._io import TerminalWriter
from _pytest._io.wcwidth import wcswidth
from _pytest.compat import order_preserving_dict
from _pytest.config import Config
from _pytest.config import ExitCode
@ -227,7 +228,7 @@ def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]:
@attr.s
class WarningReport:
"""
Simple structure to hold warnings information captured by ``pytest_warning_captured``.
Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
:ivar str message: user friendly message about the warning
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
@ -411,14 +412,12 @@ class TerminalReporter:
self.write_line("INTERNALERROR> " + line)
return 1
def pytest_warning_captured(self, warning_message, item):
# from _pytest.nodes import get_fslocation_from_item
def pytest_warning_recorded(self, warning_message, nodeid):
from _pytest.warnings import warning_record_to_str
fslocation = warning_message.filename, warning_message.lineno
message = warning_record_to_str(warning_message)
nodeid = item.nodeid if item is not None else ""
warning_report = WarningReport(
fslocation=fslocation, message=message, nodeid=nodeid
)
@ -443,8 +442,7 @@ class TerminalReporter:
self.write_ensure_prefix(line, "")
self.flush()
elif self.showfspath:
fsid = nodeid.split("::")[0]
self.write_fspath_result(fsid, "")
self.write_fspath_result(nodeid, "")
self.flush()
def pytest_runtest_logreport(self, report: TestReport) -> None:
@ -474,10 +472,7 @@ class TerminalReporter:
else:
markup = {}
if self.verbosity <= 0:
if not running_xdist and self.showfspath:
self.write_fspath_result(rep.nodeid, letter, **markup)
else:
self._tw.write(letter, **markup)
self._tw.write(letter, **markup)
else:
self._progress_nodeids_reported.add(rep.nodeid)
line = self._locationline(rep.nodeid, *rep.location)
@ -1126,8 +1121,6 @@ def _get_pos(config, rep):
def _get_line_with_reprcrash_message(config, rep, termwidth):
"""Get summary line for a report, trying to add reprcrash message."""
from wcwidth import wcswidth
verbose_word = rep._get_verbose_word(config)
pos = _get_pos(config, rep)

View File

@ -41,7 +41,7 @@ class UnitTestCase(Class):
if not getattr(cls, "__test__", True):
return
skipped = getattr(cls, "__unittest_skip__", False)
skipped = _is_skipped(cls)
if not skipped:
self._inject_setup_teardown_fixtures(cls)
self._inject_setup_class_fixture()
@ -89,7 +89,7 @@ def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self):
@pytest.fixture(scope=scope, autouse=True)
def fixture(self, request):
if getattr(self, "__unittest_skip__", None):
if _is_skipped(self):
reason = self.__unittest_skip_why__
pytest.skip(reason)
if setup is not None:
@ -220,7 +220,7 @@ class TestCaseFunction(Function):
# arguably we could always postpone tearDown(), but this changes the moment where the
# TestCase instance interacts with the results object, so better to only do it
# when absolutely needed
if self.config.getoption("usepdb"):
if self.config.getoption("usepdb") and not _is_skipped(self.obj):
self._explicit_tearDown = self._testcase.tearDown
setattr(self._testcase, "tearDown", lambda *args: None)
@ -301,3 +301,8 @@ def check_testcase_implements_trial_reporter(done=[]):
classImplements(TestCaseFunction, IReporter)
done.append(1)
def _is_skipped(obj) -> bool:
"""Return True if the given object has been marked with @unittest.skip"""
return bool(getattr(obj, "__unittest_skip__", False))

View File

@ -81,7 +81,7 @@ def catch_warnings_for_item(config, ihook, when, item):
``item`` can be None if we are not in the context of an item execution.
Each warning captured triggers the ``pytest_warning_captured`` hook.
Each warning captured triggers the ``pytest_warning_recorded`` hook.
"""
cmdline_filters = config.getoption("pythonwarnings") or []
inifilters = config.getini("filterwarnings")
@ -102,6 +102,7 @@ def catch_warnings_for_item(config, ihook, when, item):
for arg in cmdline_filters:
warnings.filterwarnings(*_parse_filter(arg, escape=True))
nodeid = "" if item is None else item.nodeid
if item is not None:
for mark in item.iter_markers(name="filterwarnings"):
for arg in mark.args:
@ -113,6 +114,14 @@ def catch_warnings_for_item(config, ihook, when, item):
ihook.pytest_warning_captured.call_historic(
kwargs=dict(warning_message=warning_message, when=when, item=item)
)
ihook.pytest_warning_recorded.call_historic(
kwargs=dict(
warning_message=warning_message,
nodeid=nodeid,
when=when,
location=None,
)
)
def warning_record_to_str(warning_message):
@ -166,7 +175,7 @@ def pytest_sessionfinish(session):
def _issue_warning_captured(warning, hook, stacklevel):
"""
This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured
at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded
hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891.
:param warning: the warning instance.
@ -185,3 +194,8 @@ def _issue_warning_captured(warning, hook, stacklevel):
warning_message=records[0], when="config", item=None, location=location
)
)
hook.pytest_warning_recorded.call_historic(
kwargs=dict(
warning_message=records[0], when="config", nodeid="", location=location
)
)

View File

@ -0,0 +1,38 @@
import pytest
from _pytest._io.wcwidth import wcswidth
from _pytest._io.wcwidth import wcwidth
@pytest.mark.parametrize(
("c", "expected"),
[
("\0", 0),
("\n", -1),
("a", 1),
("1", 1),
("א", 1),
("\u200B", 0),
("\u1ABE", 0),
("\u0591", 0),
("🉐", 2),
("", 2),
],
)
def test_wcwidth(c: str, expected: int) -> None:
assert wcwidth(c) == expected
@pytest.mark.parametrize(
("s", "expected"),
[
("", 0),
("hello, world!", 13),
("hello, world!\n", -1),
("0123456789", 10),
("שלום, עולם!", 11),
("שְבֻעָיים", 6),
("🉐🉐🉐", 6),
],
)
def test_wcswidth(s: str, expected: int) -> None:
assert wcswidth(s) == expected

View File

@ -3,6 +3,7 @@ import os
import re
import pytest
from _pytest.pytester import Testdir
def test_nothing_logged(testdir):
@ -1101,3 +1102,48 @@ def test_colored_ansi_esc_caplogtext(testdir):
)
result = testdir.runpytest("--log-level=INFO", "--color=yes")
assert result.ret == 0
def test_logging_emit_error(testdir: Testdir) -> None:
"""
An exception raised during emit() should fail the test.
The default behavior of logging is to print "Logging error"
to stderr with the call stack and some extra details.
pytest overrides this behavior to propagate the exception.
"""
testdir.makepyfile(
"""
import logging
def test_bad_log():
logging.warning('oops', 'first', 2)
"""
)
result = testdir.runpytest()
result.assert_outcomes(failed=1)
result.stdout.fnmatch_lines(
[
"====* FAILURES *====",
"*not all arguments converted during string formatting*",
]
)
def test_logging_emit_error_supressed(testdir: Testdir) -> None:
"""
If logging is configured to silently ignore errors, pytest
doesn't propagate errors either.
"""
testdir.makepyfile(
"""
import logging
def test_bad_log(monkeypatch):
monkeypatch.setattr(logging, 'raiseExceptions', False)
logging.warning('oops', 'first', 2)
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=1)

View File

@ -1251,7 +1251,7 @@ def test_syntax_error_with_non_ascii_chars(testdir):
result.stdout.fnmatch_lines(["*ERROR collecting*", "*SyntaxError*", "*1 error in*"])
def test_collecterror_with_fulltrace(testdir):
def test_collect_error_with_fulltrace(testdir):
testdir.makepyfile("assert 0")
result = testdir.runpytest("--fulltrace")
result.stdout.fnmatch_lines(
@ -1259,15 +1259,12 @@ def test_collecterror_with_fulltrace(testdir):
"collected 0 items / 1 error",
"",
"*= ERRORS =*",
"*_ ERROR collecting test_collecterror_with_fulltrace.py _*",
"",
"*/_pytest/python.py:*: ",
"_ _ _ _ _ _ _ _ *",
"*_ ERROR collecting test_collect_error_with_fulltrace.py _*",
"",
"> assert 0",
"E assert 0",
"",
"test_collecterror_with_fulltrace.py:1: AssertionError",
"test_collect_error_with_fulltrace.py:1: AssertionError",
"*! Interrupted: 1 error during collection !*",
]
)

View File

@ -19,16 +19,28 @@ from _pytest.config import ExitCode
# pylib 1.4.20.dev2 (rev 13d9af95547e)
def StdCaptureFD(out=True, err=True, in_=True):
return capture.MultiCapture(out, err, in_, Capture=capture.FDCapture)
def StdCaptureFD(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture:
return capture.MultiCapture(
in_=capture.FDCapture(0) if in_ else None,
out=capture.FDCapture(1) if out else None,
err=capture.FDCapture(2) if err else None,
)
def StdCapture(out=True, err=True, in_=True):
return capture.MultiCapture(out, err, in_, Capture=capture.SysCapture)
def StdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture:
return capture.MultiCapture(
in_=capture.SysCapture(0) if in_ else None,
out=capture.SysCapture(1) if out else None,
err=capture.SysCapture(2) if err else None,
)
def TeeStdCapture(out=True, err=True, in_=True):
return capture.MultiCapture(out, err, in_, Capture=capture.TeeSysCapture)
def TeeStdCapture(out: bool = True, err: bool = True, in_: bool = True) -> MultiCapture:
return capture.MultiCapture(
in_=capture.SysCapture(0, tee=True) if in_ else None,
out=capture.SysCapture(1, tee=True) if out else None,
err=capture.SysCapture(2, tee=True) if err else None,
)
class TestCaptureManager:
@ -866,9 +878,8 @@ class TestFDCapture:
cap = capture.FDCapture(fd)
data = b"hello"
os.write(fd, data)
s = cap.snap()
pytest.raises(AssertionError, cap.snap)
cap.done()
assert not s
cap = capture.FDCapture(fd)
cap.start()
os.write(fd, data)
@ -889,7 +900,7 @@ class TestFDCapture:
fd = tmpfile.fileno()
cap = capture.FDCapture(fd)
cap.done()
pytest.raises(ValueError, cap.start)
pytest.raises(AssertionError, cap.start)
def test_stderr(self):
cap = capture.FDCapture(2)
@ -940,11 +951,11 @@ class TestFDCapture:
assert s == "but now yes\n"
cap.suspend()
cap.done()
pytest.raises(AttributeError, cap.suspend)
pytest.raises(AssertionError, cap.suspend)
assert repr(cap) == (
"<FDCapture 1 oldfd=<UNSET> _state='done' tmpfile={!r}>".format(
cap.tmpfile
"<FDCapture 1 oldfd={} _state='done' tmpfile={!r}>".format(
cap.targetfd_save, cap.tmpfile
)
)
# Should not crash with missing "_old".
@ -1142,6 +1153,7 @@ class TestStdCaptureFD(TestStdCapture):
with lsof_check():
for i in range(10):
cap = StdCaptureFD()
cap.start_capturing()
cap.stop_capturing()
@ -1150,27 +1162,38 @@ class TestStdCaptureFDinvalidFD:
testdir.makepyfile(
"""
import os
from fnmatch import fnmatch
from _pytest import capture
def StdCaptureFD(out=True, err=True, in_=True):
return capture.MultiCapture(out, err, in_, Capture=capture.FDCapture)
return capture.MultiCapture(
in_=capture.FDCapture(0) if in_ else None,
out=capture.FDCapture(1) if out else None,
err=capture.FDCapture(2) if err else None,
)
def test_stdout():
os.close(1)
cap = StdCaptureFD(out=True, err=False, in_=False)
assert repr(cap.out) == "<FDCapture 1 oldfd=<UNSET> _state=None tmpfile=<UNSET>>"
assert fnmatch(repr(cap.out), "<FDCapture 1 oldfd=* _state='initialized' tmpfile=*>")
cap.start_capturing()
os.write(1, b"stdout")
assert cap.readouterr() == ("stdout", "")
cap.stop_capturing()
def test_stderr():
os.close(2)
cap = StdCaptureFD(out=False, err=True, in_=False)
assert repr(cap.err) == "<FDCapture 2 oldfd=<UNSET> _state=None tmpfile=<UNSET>>"
assert fnmatch(repr(cap.err), "<FDCapture 2 oldfd=* _state='initialized' tmpfile=*>")
cap.start_capturing()
os.write(2, b"stderr")
assert cap.readouterr() == ("", "stderr")
cap.stop_capturing()
def test_stdin():
os.close(0)
cap = StdCaptureFD(out=False, err=False, in_=True)
assert repr(cap.in_) == "<FDCapture 0 oldfd=<UNSET> _state=None tmpfile=<UNSET>>"
assert fnmatch(repr(cap.in_), "<FDCapture 0 oldfd=* _state='initialized' tmpfile=*>")
cap.stop_capturing()
"""
)
@ -1178,6 +1201,37 @@ class TestStdCaptureFDinvalidFD:
assert result.ret == 0
assert result.parseoutcomes()["passed"] == 3
def test_fdcapture_invalid_fd_with_fd_reuse(self, testdir):
with saved_fd(1):
os.close(1)
cap = capture.FDCaptureBinary(1)
cap.start()
os.write(1, b"started")
cap.suspend()
os.write(1, b" suspended")
cap.resume()
os.write(1, b" resumed")
assert cap.snap() == b"started resumed"
cap.done()
with pytest.raises(OSError):
os.write(1, b"done")
def test_fdcapture_invalid_fd_without_fd_reuse(self, testdir):
with saved_fd(1), saved_fd(2):
os.close(1)
os.close(2)
cap = capture.FDCaptureBinary(2)
cap.start()
os.write(2, b"started")
cap.suspend()
os.write(2, b" suspended")
cap.resume()
os.write(2, b" resumed")
assert cap.snap() == b"started resumed"
cap.done()
with pytest.raises(OSError):
os.write(2, b"done")
def test_capture_not_started_but_reset():
capsys = StdCapture()
@ -1201,11 +1255,8 @@ def test_capsys_results_accessible_by_attribute(capsys):
assert capture_result.err == "eggs"
@pytest.mark.parametrize("use", [True, False])
def test_fdcapture_tmpfile_remains_the_same(tmpfile, use):
if not use:
tmpfile = True
cap = StdCaptureFD(out=False, err=tmpfile)
def test_fdcapture_tmpfile_remains_the_same() -> None:
cap = StdCaptureFD(out=False, err=True)
try:
cap.start_capturing()
capfile = cap.err.tmpfile
@ -1238,16 +1289,21 @@ def test_close_and_capture_again(testdir):
)
@pytest.mark.parametrize("method", ["SysCapture", "FDCapture", "TeeSysCapture"])
def test_capturing_and_logging_fundamentals(testdir, method):
@pytest.mark.parametrize(
"method", ["SysCapture(2)", "SysCapture(2, tee=True)", "FDCapture(2)"]
)
def test_capturing_and_logging_fundamentals(testdir, method: str) -> None:
# here we check a fundamental feature
p = testdir.makepyfile(
"""
import sys, os
import py, logging
from _pytest import capture
cap = capture.MultiCapture(out=False, in_=False,
Capture=capture.%s)
cap = capture.MultiCapture(
in_=None,
out=None,
err=capture.%s,
)
cap.start_capturing()
logging.warning("hello1")

View File

@ -147,6 +147,70 @@ class TestParseIni:
result = testdir.inline_run("--confcutdir=.")
assert result.ret == 0
@pytest.mark.parametrize(
"ini_file_text, invalid_keys, stderr_output, exception_text",
[
(
"""
[pytest]
unknown_ini = value1
another_unknown_ini = value2
""",
["unknown_ini", "another_unknown_ini"],
[
"WARNING: Unknown config ini key: unknown_ini",
"WARNING: Unknown config ini key: another_unknown_ini",
],
"Unknown config ini key: unknown_ini",
),
(
"""
[pytest]
unknown_ini = value1
minversion = 5.0.0
""",
["unknown_ini"],
["WARNING: Unknown config ini key: unknown_ini"],
"Unknown config ini key: unknown_ini",
),
(
"""
[some_other_header]
unknown_ini = value1
[pytest]
minversion = 5.0.0
""",
[],
[],
"",
),
(
"""
[pytest]
minversion = 5.0.0
""",
[],
[],
"",
),
],
)
def test_invalid_ini_keys(
self, testdir, ini_file_text, invalid_keys, stderr_output, exception_text
):
testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text))
config = testdir.parseconfig()
assert config._get_unknown_ini_keys() == invalid_keys, str(
config._get_unknown_ini_keys()
)
result = testdir.runpytest()
result.stderr.fnmatch_lines(stderr_output)
if stderr_output:
with pytest.raises(pytest.fail.Exception, match=exception_text):
testdir.runpytest("--strict-config")
class TestConfigCmdlineParsing:
def test_parsing_again_fails(self, testdir):
@ -1243,9 +1307,7 @@ def test_help_and_version_after_argument_error(testdir):
assert result.ret == ExitCode.USAGE_ERROR
result = testdir.runpytest("--version")
result.stderr.fnmatch_lines(
["*pytest*{}*imported from*".format(pytest.__version__)]
)
result.stderr.fnmatch_lines(["pytest {}".format(pytest.__version__)])
assert result.ret == ExitCode.USAGE_ERROR

View File

@ -342,6 +342,15 @@ class TestPDB:
child.sendeof()
self.flush(child)
def test_pdb_prevent_ConftestImportFailure_hiding_exception(self, testdir):
testdir.makepyfile("def test_func(): pass")
sub_dir = testdir.tmpdir.join("ns").ensure_dir()
sub_dir.join("conftest").new(ext=".py").write("import unknown")
sub_dir.join("test_file").new(ext=".py").write("def test_func(): pass")
result = testdir.runpytest_subprocess("--pdb", ".")
result.stdout.fnmatch_lines(["-> import unknown"])
def test_pdb_interaction_capturing_simple(self, testdir):
p1 = testdir.makepyfile(
"""

View File

@ -2,11 +2,10 @@ import pytest
from _pytest.config import ExitCode
def test_version(testdir, pytestconfig):
def test_version_verbose(testdir, pytestconfig):
testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
result = testdir.runpytest("--version")
result = testdir.runpytest("--version", "--version")
assert result.ret == 0
# p = py.path.local(py.__file__).dirpath()
result.stderr.fnmatch_lines(
["*pytest*{}*imported from*".format(pytest.__version__)]
)
@ -14,6 +13,14 @@ def test_version(testdir, pytestconfig):
result.stderr.fnmatch_lines(["*setuptools registered plugins:", "*at*"])
def test_version_less_verbose(testdir, pytestconfig):
testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
result = testdir.runpytest("--version")
assert result.ret == 0
# p = py.path.local(py.__file__).dirpath()
result.stderr.fnmatch_lines(["pytest {}".format(pytest.__version__)])
def test_help(testdir):
result = testdir.runpytest("--help")
assert result.ret == 0

View File

@ -58,3 +58,30 @@ def test__check_initialpaths_for_relpath():
outside = py.path.local("/outside")
assert nodes._check_initialpaths_for_relpath(FakeSession, outside) is None
def test_failure_with_changed_cwd(testdir):
"""
Test failure lines should use absolute paths if cwd has changed since
invocation, so the path is correct (#6428).
"""
p = testdir.makepyfile(
"""
import os
import pytest
@pytest.fixture
def private_dir():
out_dir = 'ddd'
os.mkdir(out_dir)
old_dir = os.getcwd()
os.chdir(out_dir)
yield out_dir
os.chdir(old_dir)
def test_show_wrong_path(private_dir):
assert False
"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines([str(p) + ":*: AssertionError", "*1 failed in *"])

View File

@ -5,6 +5,7 @@ import py
import pytest
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import get_extended_length_path_str
from _pytest.pathlib import get_lock_path
from _pytest.pathlib import maybe_delete_a_numbered_dir
from _pytest.pathlib import Path
@ -89,3 +90,26 @@ def test_access_denied_during_cleanup(tmp_path, monkeypatch):
lock_path = get_lock_path(path)
maybe_delete_a_numbered_dir(path)
assert not lock_path.is_file()
def test_long_path_during_cleanup(tmp_path):
"""Ensure that deleting long path works (particularly on Windows (#6775))."""
path = (tmp_path / ("a" * 250)).resolve()
if sys.platform == "win32":
# make sure that the full path is > 260 characters without any
# component being over 260 characters
assert len(str(path)) > 260
extended_path = "\\\\?\\" + str(path)
else:
extended_path = str(path)
os.mkdir(extended_path)
assert os.path.isdir(extended_path)
maybe_delete_a_numbered_dir(path)
assert not os.path.isdir(extended_path)
def test_get_extended_length_path_str():
assert get_extended_length_path_str(r"c:\foo") == r"\\?\c:\foo"
assert get_extended_length_path_str(r"\\share\foo") == r"\\?\UNC\share\foo"
assert get_extended_length_path_str(r"\\?\UNC\share\foo") == r"\\?\UNC\share\foo"
assert get_extended_length_path_str(r"\\?\c:\foo") == r"\\?\c:\foo"

View File

@ -396,6 +396,14 @@ class TestReportSerialization:
# for same reasons as previous test, ensure we don't blow up here
loaded_report.longrepr.toterminal(tw_mock)
def test_report_prevent_ConftestImportFailure_hiding_exception(self, testdir):
sub_dir = testdir.tmpdir.join("ns").ensure_dir()
sub_dir.join("conftest").new(ext=".py").write("import unknown")
result = testdir.runpytest_subprocess(".")
result.stdout.fnmatch_lines(["E *Error: No module named 'unknown'"])
result.stdout.no_fnmatch_line("ERROR - *ConftestImportFailure*")
class TestHooks:
"""Test that the hooks are working correctly for plugins"""

View File

@ -1002,6 +1002,17 @@ class TestReportContents:
assert rep.capstdout == ""
assert rep.capstderr == ""
def test_longrepr_type(self, testdir) -> None:
reports = testdir.runitem(
"""
import pytest
def test_func():
pytest.fail(pytrace=False)
"""
)
rep = reports[1]
assert isinstance(rep.longrepr, _pytest._code.code.ExceptionRepr)
def test_outcome_exception_bad_msg() -> None:
"""Check that OutcomeExceptions validate their input to prevent confusing errors (#5578)"""

View File

@ -194,7 +194,7 @@ class TestXFail:
assert len(reports) == 3
callreport = reports[1]
assert callreport.failed
assert callreport.longrepr == "[XPASS(strict)] nope"
assert str(callreport.longrepr) == "[XPASS(strict)] nope"
assert not hasattr(callreport, "wasxfail")
def test_xfail_run_anyway(self, testdir):

View File

@ -14,7 +14,9 @@ import pluggy
import py
import _pytest.config
import _pytest.terminal
import pytest
from _pytest._io.wcwidth import wcswidth
from _pytest.config import ExitCode
from _pytest.pytester import Testdir
from _pytest.reports import BaseReport
@ -2027,9 +2029,6 @@ def test_skip_reasons_folding():
def test_line_with_reprcrash(monkeypatch):
import _pytest.terminal
from wcwidth import wcswidth
mocked_verbose_word = "FAILED"
mocked_pos = "some::nodeid"
@ -2079,19 +2078,19 @@ def test_line_with_reprcrash(monkeypatch):
check("some\nmessage", 80, "FAILED some::nodeid - some")
# Test unicode safety.
check("😄😄😄😄😄\n2nd line", 25, "FAILED some::nodeid - ...")
check("😄😄😄😄😄\n2nd line", 26, "FAILED some::nodeid - ...")
check("😄😄😄😄😄\n2nd line", 27, "FAILED some::nodeid - 😄...")
check("😄😄😄😄😄\n2nd line", 28, "FAILED some::nodeid - 😄...")
check("😄😄😄😄😄\n2nd line", 29, "FAILED some::nodeid - 😄😄...")
check("🉐🉐🉐🉐🉐\n2nd line", 25, "FAILED some::nodeid - ...")
check("🉐🉐🉐🉐🉐\n2nd line", 26, "FAILED some::nodeid - ...")
check("🉐🉐🉐🉐🉐\n2nd line", 27, "FAILED some::nodeid - 🉐...")
check("🉐🉐🉐🉐🉐\n2nd line", 28, "FAILED some::nodeid - 🉐...")
check("🉐🉐🉐🉐🉐\n2nd line", 29, "FAILED some::nodeid - 🉐🉐...")
# NOTE: constructed, not sure if this is supported.
mocked_pos = "nodeid::😄::withunicode"
check("😄😄😄😄😄\n2nd line", 29, "FAILED nodeid::😄::withunicode")
check("😄😄😄😄😄\n2nd line", 40, "FAILED nodeid::😄::withunicode - 😄😄...")
check("😄😄😄😄😄\n2nd line", 41, "FAILED nodeid::😄::withunicode - 😄😄...")
check("😄😄😄😄😄\n2nd line", 42, "FAILED nodeid::😄::withunicode - 😄😄😄...")
check("😄😄😄😄😄\n2nd line", 80, "FAILED nodeid::😄::withunicode - 😄😄😄😄😄")
mocked_pos = "nodeid::🉐::withunicode"
check("🉐🉐🉐🉐🉐\n2nd line", 29, "FAILED nodeid::🉐::withunicode")
check("🉐🉐🉐🉐🉐\n2nd line", 40, "FAILED nodeid::🉐::withunicode - 🉐🉐...")
check("🉐🉐🉐🉐🉐\n2nd line", 41, "FAILED nodeid::🉐::withunicode - 🉐🉐...")
check("🉐🉐🉐🉐🉐\n2nd line", 42, "FAILED nodeid::🉐::withunicode - 🉐🉐🉐...")
check("🉐🉐🉐🉐🉐\n2nd line", 80, "FAILED nodeid::🉐::withunicode - 🉐🉐🉐🉐🉐")
@pytest.mark.parametrize(

View File

@ -1193,6 +1193,40 @@ def test_pdb_teardown_called(testdir, monkeypatch):
]
@pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"])
def test_pdb_teardown_skipped(testdir, monkeypatch, mark):
"""
With --pdb, setUp and tearDown should not be called for skipped tests.
"""
tracked = []
monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False)
testdir.makepyfile(
"""
import unittest
import pytest
class MyTestCase(unittest.TestCase):
def setUp(self):
pytest.test_pdb_teardown_skipped.append("setUp:" + self.id())
def tearDown(self):
pytest.test_pdb_teardown_skipped.append("tearDown:" + self.id())
{mark}("skipped for reasons")
def test_1(self):
pass
""".format(
mark=mark
)
)
result = testdir.runpytest_inprocess("--pdb")
result.stdout.fnmatch_lines("* 1 skipped in *")
assert tracked == []
def test_async_support(testdir):
pytest.importorskip("unittest.async_case")

View File

@ -268,9 +268,8 @@ def test_warning_captured_hook(testdir):
collected = []
class WarningCollector:
def pytest_warning_captured(self, warning_message, when, item):
imge_name = item.name if item is not None else ""
collected.append((str(warning_message.message), when, imge_name))
def pytest_warning_recorded(self, warning_message, when, nodeid, location):
collected.append((str(warning_message.message), when, nodeid, location))
result = testdir.runpytest(plugins=[WarningCollector()])
result.stdout.fnmatch_lines(["*1 passed*"])
@ -278,11 +277,27 @@ def test_warning_captured_hook(testdir):
expected = [
("config warning", "config", ""),
("collect warning", "collect", ""),
("setup warning", "runtest", "test_func"),
("call warning", "runtest", "test_func"),
("teardown warning", "runtest", "test_func"),
("setup warning", "runtest", "test_warning_captured_hook.py::test_func"),
("call warning", "runtest", "test_warning_captured_hook.py::test_func"),
("teardown warning", "runtest", "test_warning_captured_hook.py::test_func"),
]
assert collected == expected
for index in range(len(expected)):
collected_result = collected[index]
expected_result = expected[index]
assert collected_result[0] == expected_result[0], str(collected)
assert collected_result[1] == expected_result[1], str(collected)
assert collected_result[2] == expected_result[2], str(collected)
# NOTE: collected_result[3] is location, which differs based on the platform you are on
# thus, the best we can do here is assert the types of the paremeters match what we expect
# and not try and preload it in the expected array
if collected_result[3] is not None:
assert type(collected_result[3][0]) is str, str(collected)
assert type(collected_result[3][1]) is int, str(collected)
assert type(collected_result[3][2]) is str, str(collected)
else:
assert collected_result[3] is None, str(collected)
@pytest.mark.filterwarnings("always")
@ -649,7 +664,7 @@ class TestStackLevel:
captured = []
@classmethod
def pytest_warning_captured(cls, warning_message, when, item, location):
def pytest_warning_recorded(cls, warning_message, when, nodeid, location):
cls.captured.append((warning_message, location))
testdir.plugins = [CapturedWarnings()]

View File

@ -80,9 +80,8 @@ usedevelop = True
deps =
-r{toxinidir}/doc/en/requirements.txt
towncrier
whitelist_externals = sh
commands =
sh -c 'towncrier --draft > doc/en/_changelog_towncrier_draft.rst'
python scripts/towncrier-draft-to-file.py
# the '-t changelog_towncrier_draft' tags makes sphinx include the draft
# changelog in the docs; this does not happen on ReadTheDocs because it uses
# the standard sphinx command so the 'changelog_towncrier_draft' is never set there