Merge branch 'master' into fix-flaky-test
This commit is contained in:
commit
7a8b8a5c9f
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
4
AUTHORS
4
AUTHORS
|
@ -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
|
||||
|
|
|
@ -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)``.
|
|
@ -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.
|
|
@ -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``.
|
|
@ -0,0 +1 @@
|
|||
Support deleting paths longer than 260 characters on windows created inside tmpdir.
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
Prevent pytest from printing ConftestImportFailure traceback to stdout.
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
`pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins.
|
|
@ -0,0 +1 @@
|
|||
Prevent hiding the underlying exception when ``ConfTestImportFailure`` is raised.
|
|
@ -0,0 +1 @@
|
|||
The development guide now links to the contributing section of the docs and 'RELEASING.rst' on GitHub.
|
|
@ -0,0 +1,2 @@
|
|||
Fix regression where running with ``--pdb`` would call the ``tearDown`` methods of ``unittest.TestCase``
|
||||
subclasses for skipped tests.
|
|
@ -0,0 +1 @@
|
|||
Add a note about ``--strict`` and ``--strict-markers`` and the preference for the latter one.
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
The dependency on the ``wcwidth`` package has been removed.
|
|
@ -1 +1,6 @@
|
|||
comment: off
|
||||
# reference: https://docs.codecov.io/docs/codecovyml-reference
|
||||
coverage:
|
||||
status:
|
||||
patch: true
|
||||
project: false
|
||||
comment: false
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
*.pyc
|
||||
*.pyo
|
||||
.DS_Store
|
|
@ -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.
|
|
@ -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
|
|
@ -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">
|
||||
© Copyright {{ copyright }}.
|
||||
Created using <a href="https://www.sphinx-doc.org/">Sphinx</a> {{ sphinx_version }}.
|
||||
</div>
|
||||
{% if pagename == 'index' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endblock %}
|
|
@ -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;
|
||||
}
|
|
@ -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 =
|
|
@ -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'
|
||||
}
|
|
@ -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.
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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>`_.
|
||||
|
|
|
@ -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
|
||||
--------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
1
setup.py
1
setup.py
|
@ -12,7 +12,6 @@ INSTALL_REQUIRES = [
|
|||
'colorama;sys_platform=="win32"',
|
||||
"pluggy>=0.12,<1.0",
|
||||
'importlib-metadata>=0.12;python_version<"3.8"',
|
||||
"wcwidth",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
@ -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(
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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."
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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 !*",
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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(
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 *"])
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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)"""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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()]
|
||||
|
|
3
tox.ini
3
tox.ini
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue