Compare commits

...

66 Commits
5.1.2 ... 5.2.0

Author SHA1 Message Date
Bruno Oliveira
068ef90b92 Preparing release version 5.2.0 2019-09-28 21:18:37 -04:00
Bruno Oliveira
065773aa97 Use 'python3' instead of 'python3.6' on tox
This allows us to use python3.7+ to use tox
2019-09-28 21:16:20 -04:00
Anthony Sottile
b62276826c Merge pull request #5886 from nicoddemus/setup-plan-custom-items-5884
Fix --setup-only and --setup-show for custom pytest items
2019-09-28 18:08:36 -07:00
Bruno Oliveira
7bdfba3578 Fix --setup-only and --setup-show for custom pytest items
Fix #5884
2019-09-28 11:52:09 -03:00
Daniel Hahler
6bfd30d169 Merge pull request #5878 from blueyed/codecov-retry-6
ci: codecov: use 6 retries with curl
2019-09-24 10:30:23 +02:00
Daniel Hahler
7731e45615 ci: codecov: use 6 retries with curl
This should result in retries of 1+2+4+8+16+32 = 63s.

Ref: https://github.com/pytest-dev/pytest/pull/5869#issuecomment-534235437
2019-09-23 20:58:52 +02:00
Anthony Sottile
8806b1f531 Merge pull request #5875 from asottile/patch-1
Make sure to quote `sys.executable` as we're running a shell
2019-09-23 18:55:33 +02:00
Anthony Sottile
19c9e53604 Make sure to quote sys.executable as we're running a shell 2019-09-23 08:58:56 -07:00
Bruno Oliveira
c28b63135f Merge master into features (#5874)
Merge master into features
2019-09-23 12:44:20 -03:00
Bruno Oliveira
7c64d5d882 Do not call python directly but use sys.executable. Fixes #5872 (#5873)
Do not call python directly but use sys.executable. Fixes #5872
2019-09-23 12:28:00 -03:00
Bruno Oliveira
d3d9f9f668 Merge remote-tracking branch 'upstream/master' into mm 2019-09-23 12:09:01 -03:00
Bruno Oliveira
018edf2a0e Change report-coverage.sh in attempt to fix Azure (#5869)
Change report-coverage.sh in attempt to fix Azure
2019-09-23 12:01:19 -03:00
Tomáš Chvátal
04c01fb606 test_argcomplete do not call python directly #5872
Use sys.executable to detect which python we should actually be testing.
2019-09-23 16:38:15 +02:00
Daniel Hahler
ea0c7e43b6 Remove unneeded codecov options (implied with "-f") 2019-09-23 02:26:53 +02:00
Bruno Oliveira
de8fdab7a9 Change report-coverage.sh in attempt to fix Azure
Recently sometimes Azure has failed with:

++ curl -s https://codecov.io/bash
bash: /dev/fd/63: No such file or directory

This attempts to fix this by modifying report-coverage.sh slightly.
2019-09-21 16:02:48 -03:00
Daniel Hahler
c1361b48f8 Merge pull request #5868 from blueyed/mm
Merge master into features
2019-09-21 16:29:49 +02:00
Bruno Oliveira
1b4ad7774b Fix logging doc: change x.level to x.levelno (#5866)
Fix logging doc: change x.level to x.levelno
2019-09-21 11:28:40 -03:00
Daniel Hahler
409cc2946a Merge master into features 2019-09-21 16:22:48 +02:00
Bruno Oliveira
3114be9181 Revert "Show banner/full page for the Digital Climate Strike (#… (#5867)
Revert "Show banner/full page for the Digital Climate Strike (#5861)"
2019-09-21 11:09:34 -03:00
Bruno Oliveira
e4103cb02c Release version 5.1.3 (#5859)
Release version 5.1.3
2019-09-21 10:56:16 -03:00
Bruno Oliveira
217605c217 Revert "Show banner/full page for the Digital Climate Strike (#5861)"
This reverts commit c8cf748c49, reversing
changes made to 702acdba46.
2019-09-21 10:38:10 -03:00
James Cooke
2fcf21a6c7 Fix logging doc: change x.level to x.levelno 2019-09-20 18:38:47 +01:00
Bruno Oliveira
c8cf748c49 Show banner/full page for the Digital Climate Strike (#5861)
Show banner/full page for the Digital Climate Strike
2019-09-19 10:49:10 -03:00
Bruno Oliveira
249b53e623 Show banner/full page for the Ditigal Climate Strike
As discussed in the mailing list, pytest will join the
digital strike for the climate on Sep 20th. This will show
a closable banner on the docs until the date, and when the date
comes the banner will become a (closable) full page.

I will also pin an issue saying that the developers won't be
available on Sep 20th.
2019-09-19 08:26:25 -03:00
Bruno Oliveira
9669413b1f Merge pull request #5776 from aklajnert/1682-dynamic-scope
Implemented the dynamic scope feature.
2019-09-19 08:22:45 -03:00
Andrzej Klajnert
e2382e96ed Minor cleanup in tests. 2019-09-19 11:13:22 +02:00
Bruno Oliveira
1a9f4a51cb Preparing release version 5.1.3 2019-09-18 10:11:59 -03:00
Bruno Oliveira
892bdd59dc Normalize all summary durations, including quiet ones 2019-09-18 10:10:25 -03:00
Bruno Oliveira
df46afc96d Change fixture argument handling tests to unit-tests 2019-09-18 07:50:35 -03:00
Bruno Oliveira
6918d07560 Merge remote-tracking branch 'upstream/features' into aklajnert/1682-dynamic-scope 2019-09-18 07:44:18 -03:00
Daniel Hahler
c997c32004 Merge pull request #5856 from blueyed/mm
Merge master into features
2019-09-17 21:47:40 +02:00
Daniel Hahler
450409d123 Merge master into features
Conflicts:
	src/_pytest/reports.py
        (via 7259c453d, moved the type annotation; setting it to `None`
        was removed in 3c82b1cb9 already)
2019-09-17 12:46:36 +02:00
Daniel Hahler
702acdba46 Merge pull request #5811 from blueyed/fulltrace-pytest-raises
Handle --fulltrace with pytest.raises
2019-09-14 02:09:36 +02:00
Daniel Hahler
f832ac3316 Handle --fulltrace with pytest.raises
This changes `_repr_failure_py` to use `tbfilter=False` always.
2019-09-14 01:41:43 +02:00
Bruno Oliveira
9422e10322 Fix regression due to different cases on Windows (#5840)
Fix regression due to different cases on Windows
2019-09-13 18:11:12 -03:00
Bruno Oliveira
5c3b4a6f52 Add CHANGELOG entry for #5792 2019-09-12 08:05:50 -03:00
Christian Neumüller
05850d73bd Re-introduce Christian Neumüller to AUTHORS
The introduction was reverted by cd29d56
2019-09-12 08:05:50 -03:00
Bruno Oliveira
b48f51eb03 Use Path() objects to store conftest files
Using Path().resolve() is better than py.path.realpath because
it resolves to the correct path/drive in case-insensitive file systems (#5792):

>>> from py.path import local
>>> from pathlib import Path
>>>
>>> local('d:\\projects').realpath()
local('d:\\projects')
>>> Path('d:\\projects').resolve()
WindowsPath('D:/projects')

Fix #5819
2019-09-12 08:05:50 -03:00
Bruno Oliveira
cf5b544db3 Revert "Merge pull request #5792 from dynatrace-oss-contrib/bugfix/badcase"
This reverts commit 955e542210, reversing
changes made to 0215bcd84e.

Will attempt a simpler approach
2019-09-12 08:05:43 -03:00
Bruno Oliveira
73c5b7f4b1 Clarify docs by showing tox.ini considered before setup.cfg (#5839)
Clarify docs by showing tox.ini considered before setup.cfg
2019-09-11 22:14:31 -03:00
Anthony Sottile
8f2f51be6d Clarify docs by showing tox.ini considered before setup.cfg 2019-09-11 14:07:06 -07:00
Andrzej Klajnert
f2f3ced508 Fixed the fixture function signature. 2019-09-10 16:20:44 +02:00
Bruno Oliveira
23102a7d84 Update doc regarding pytest.raises (#5834)
Update doc regarding pytest.raises
2019-09-09 16:18:58 -03:00
Gene Wood
f0d538329c Update doc regarding pytest.raises
Remove reference to the `message` argument in the docs as it was deprecated in #4539
2019-09-09 12:14:09 -07:00
Bruno Oliveira
6c8bcf601c Fix pypy3.6 on windows (#5828)
Fix pypy3.6 on windows
2019-09-08 12:11:28 -03:00
Anthony Sottile
9d7b919c7d Fix pypy3.6 on windows 2019-09-07 16:49:05 -07:00
Bruno Oliveira
333e9d5c10 Merge pull request #5824 from blueyed/revert-py350
ci: Travis: do not test with 3.5.0
2019-09-06 08:05:51 -03:00
Daniel Hahler
f1b605c95e ci: Travis: do not test with 3.5.0
This causes flaky test failures (crashes).

Closes https://github.com/pytest-dev/pytest/issues/5795.
2019-09-06 12:29:17 +02:00
Bruno Oliveira
2bb8d93001 Fix for Python 4: replace unsafe PY3 with PY2 (#5820)
Fix for Python 4: replace unsafe PY3 with PY2
2019-09-05 12:39:06 -03:00
Hugo
d049b35397 Fix for Python 4: replace unsafe PY3 with PY2 2019-09-05 18:06:47 +03:00
Bruno Oliveira
8ee557f7ae Fix pythonpath anchor (#5817)
Fix pythonpath anchor
2019-09-04 22:19:57 -03:00
Gene Wood
ca3884d9bb Add Gene Wood to authors 2019-09-04 09:21:10 -07:00
Gene Wood
bc163605ab Fix anchor link from Good Practices to Pythonpath doc 2019-09-04 09:18:10 -07:00
Bruno Oliveira
1675048b35 Merge pull request #5808 from goerz/pastebin
Fix "lexer" being used when uploading to bpaste.net
2019-08-31 16:11:39 -03:00
aklajnert
10bf6aac76 Implemented the dynamic scope feature. 2019-08-31 18:12:24 +02:00
Michael Goerz
f8dd6349c1 Fix "lexer" being used when uploading to bpaste.net
Closes #5806.
2019-08-30 15:34:03 -04:00
Bruno Oliveira
8c8809e1aa Merge pull request #5805 from nicoddemus/release-5.1.2
Release 5.1.2
2019-08-30 16:05:14 -03:00
Bruno Oliveira
404cf0c872 Merge pull request #5764 from goerz/pastebin
Gracefully handle HTTP errors from pastebin
2019-08-30 07:29:14 -03:00
Michael Goerz
d47b9d04d4 Gracefully handle HTTP errors from pastebin
We find that the --pastebin option to pytest sometimes fails with "HTTP
Error 400: Bad Request". We're still investigating the exact cause of
these errors, but in the meantime, a failure to upload to the pastebin
service should probably not crash pytest and cause a test failure in the
continuous-integration.

This patch catches exceptions like HTTPError that may be thrown while
trying to communicate with the pastebin service, and reports them as a
"bad response", without crashing with a backtrace or failing the entire
test suite.
2019-08-26 23:50:46 -04:00
Anthony Sottile
5bf9f9a711 Merge pull request #5788 from nicoddemus/mm
Merge master into features
2019-08-26 18:20:07 -07:00
Bruno Oliveira
c28e428249 Merge remote-tracking branch 'upstream/master' into mm 2019-08-26 20:00:30 -03:00
Ran Benita
c2f762460f Merge pull request #5673 from bluetech/type-annotations-3
1/X Fix check_untyped_defs = True mypy errors
2019-08-20 17:20:40 +03:00
Daniel Hahler
f05ca74d27 Merge pull request #5056 from blueyed/argparsing-width
Inject width via pylib to argparse formatter
2019-08-17 21:46:02 +02:00
Daniel Hahler
2a6a1ca07d Inject width via pylib to argparse formatter
`argparse.HelpFormatter` looks at `$COLUMNS` only, falling back to a
default of 80.

`py.io.get_terminal_width()` is smarter there, and could even work
better with https://github.com/pytest-dev/py/pull/219.

This ensures to use a consistent value for formatting the ini values etc.
2019-08-17 16:51:02 +02:00
Ran Benita
7259c453d6 Fix some check_untyped_defs = True mypy warnings 2019-08-16 10:41:57 +03:00
Ran Benita
28761c8da1 Have AssertionRewritingHook derive from importlib.abc.MetaPathFinder
This is nice for self-documentation, and is the type required by mypy
for adding to sys.meta_path.
2019-08-16 10:41:52 +03:00
56 changed files with 772 additions and 209 deletions

View File

@@ -42,9 +42,8 @@ jobs:
- env: TOXENV=pypy3-xdist
python: 'pypy3'
- env: TOXENV=py35
dist: trusty
python: '3.5.0'
- env: TOXENV=py35-xdist
python: '3.5'
# Coverage for:
# - pytester's LsofFdLeakChecker

View File

@@ -98,6 +98,7 @@ Feng Ma
Florian Bruhin
Floris Bruynooghe
Gabriel Reis
Gene Wood
George Kussumoto
Georgy Dyuldin
Graham Horler
@@ -175,6 +176,7 @@ mbyt
Michael Aquilina
Michael Birtwell
Michael Droettboom
Michael Goerz
Michael Seifert
Michal Wajszczuk
Mihai Capotă

View File

@@ -18,6 +18,61 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 5.2.0 (2019-09-28)
=========================
Deprecations
------------
- `#1682 <https://github.com/pytest-dev/pytest/issues/1682>`_: Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them
as a keyword argument instead.
Features
--------
- `#1682 <https://github.com/pytest-dev/pytest/issues/1682>`_: The ``scope`` parameter of ``@pytest.fixture`` can now be a callable that receives
the fixture name and the ``config`` object as keyword-only parameters.
See `the docs <https://docs.pytest.org/en/fixture.html#dynamic-scope>`__ for more information.
- `#5764 <https://github.com/pytest-dev/pytest/issues/5764>`_: New behavior of the ``--pastebin`` option: failures to connect to the pastebin server are reported, without failing the pytest run
Bug Fixes
---------
- `#5806 <https://github.com/pytest-dev/pytest/issues/5806>`_: Fix "lexer" being used when uploading to bpaste.net from ``--pastebin`` to "text".
- `#5884 <https://github.com/pytest-dev/pytest/issues/5884>`_: Fix ``--setup-only`` and ``--setup-show`` for custom pytest items.
Trivial/Internal Changes
------------------------
- `#5056 <https://github.com/pytest-dev/pytest/issues/5056>`_: The HelpFormatter uses ``py.io.get_terminal_width`` for better width detection.
pytest 5.1.3 (2019-09-18)
=========================
Bug Fixes
---------
- `#5807 <https://github.com/pytest-dev/pytest/issues/5807>`_: Fix pypy3.6 (nightly) on windows.
- `#5811 <https://github.com/pytest-dev/pytest/issues/5811>`_: Handle ``--fulltrace`` correctly with ``pytest.raises``.
- `#5819 <https://github.com/pytest-dev/pytest/issues/5819>`_: Windows: Fix regression with conftest whose qualified name contains uppercase
characters (introduced by #5792).
pytest 5.1.2 (2019-08-30)
=========================

View File

@@ -16,7 +16,7 @@ REGENDOC_ARGS := \
--normalize "/[ \t]+\n/\n/" \
--normalize "~\$$REGENDOC_TMPDIR~/home/sweet/project~" \
--normalize "~/path/to/example~/home/sweet/project~" \
--normalize "/in \d+.\d+s ==/in 0.12s ==/" \
--normalize "/in \d.\d\ds/in 0.12s/" \
--normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \
--normalize "@pytest-(\d+)\\.[^ ,]+@pytest-\1.x.y@" \
--normalize "@(This is pytest version )(\d+)\\.[^ ,]+@\1\2.x.y@" \

View File

@@ -6,6 +6,8 @@ Release announcements
:maxdepth: 2
release-5.2.0
release-5.1.3
release-5.1.2
release-5.1.1
release-5.1.0

View File

@@ -0,0 +1,23 @@
pytest-5.1.3
=======================================
pytest 5.1.3 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
Thanks to all who contributed to this release, among them:
* Anthony Sottile
* Bruno Oliveira
* Christian Neumüller
* Daniel Hahler
* Gene Wood
* Hugo
Happy testing,
The pytest Development Team

View File

@@ -0,0 +1,36 @@
pytest-5.2.0
=======================================
The pytest team is proud to announce the 5.2.0 release!
pytest is a mature Python testing tool with more than a 2000 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
to take a look at the CHANGELOG:
https://docs.pytest.org/en/latest/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/latest/
As usual, you can upgrade from pypi via:
pip install -U pytest
Thanks to all who contributed to this release, among them:
* Andrzej Klajnert
* Anthony Sottile
* Bruno Oliveira
* Daniel Hahler
* James Cooke
* Michael Goerz
* Ran Benita
* Tomáš Chvátal
* aklajnert
Happy testing,
The Pytest Development Team

View File

@@ -279,7 +279,7 @@ the conftest file:
E vals: 1 != 2
test_foocompare.py:12: AssertionError
1 failed in 0.02s
1 failed in 0.12s
.. _assert-details:
.. _`assert introspection`:

View File

@@ -160,7 +160,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
in python < 3.6 this is a pathlib2.Path
no tests ran in 0.00s
no tests ran in 0.12s
You can also interactively ask for help, e.g. by typing on the Python interactive prompt something like:

View File

@@ -75,7 +75,7 @@ If you run this for the first time you will see two failures:
E Failed: bad luck
test_50.py:7: Failed
2 failed, 48 passed in 0.07s
2 failed, 48 passed in 0.12s
If you then run it with ``--lf``:
@@ -230,7 +230,7 @@ If you run this command for the first time, you can see the print statement:
test_caching.py:20: AssertionError
-------------------------- Captured stdout setup ---------------------------
running expensive computation...
1 failed in 0.02s
1 failed in 0.12s
If you run it a second time, the value will be retrieved from
the cache and nothing will be printed:
@@ -249,7 +249,7 @@ the cache and nothing will be printed:
E assert 42 == 23
test_caching.py:20: AssertionError
1 failed in 0.02s
1 failed in 0.12s
See the :ref:`cache-api` for more details.

View File

@@ -107,8 +107,8 @@ check for ini-files as follows:
# first look for pytest.ini files
path/pytest.ini
path/setup.cfg # must also contain [tool:pytest] section to match
path/tox.ini # must also contain [pytest] section to match
path/setup.cfg # must also contain [tool:pytest] section to match
pytest.ini
... # all the way down to the root

View File

@@ -1,7 +1,7 @@
import pytest
@pytest.fixture("session")
@pytest.fixture(scope="session")
def setup(request):
setup = CostlySetup()
yield setup

View File

@@ -499,7 +499,7 @@ The output is as follows:
$ pytest -q -s
Mark(name='my_marker', args=(<function hello_world at 0xdeadbeef>,), kwargs={})
.
1 passed in 0.01s
1 passed in 0.12s
We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``.
@@ -551,7 +551,7 @@ Let's run this without capturing output and see what we get:
glob args=('class',) kwargs={'x': 2}
glob args=('module',) kwargs={'x': 1}
.
1 passed in 0.02s
1 passed in 0.12s
marking platform specific tests with pytest
--------------------------------------------------------------

View File

@@ -54,7 +54,7 @@ This means that we only run 2 tests if we do not pass ``--all``:
$ pytest -q test_compute.py
.. [100%]
2 passed in 0.01s
2 passed in 0.12s
We run only two computations, so we see two dots.
let's run the full monty:
@@ -73,7 +73,7 @@ let's run the full monty:
E assert 4 < 4
test_compute.py:4: AssertionError
1 failed, 4 passed in 0.02s
1 failed, 4 passed in 0.12s
As expected when running the full range of ``param1`` values
we'll get an error on the last one.
@@ -343,7 +343,7 @@ And then when we run the test:
E Failed: deliberately failing for demo purposes
test_backends.py:8: Failed
1 failed, 1 passed in 0.02s
1 failed, 1 passed in 0.12s
The first invocation with ``db == "DB1"`` passed while the second with ``db == "DB2"`` failed. Our ``db`` fixture function has instantiated each of the DB values during the setup phase while the ``pytest_generate_tests`` generated two according calls to the ``test_db_initialized`` during the collection phase.
@@ -454,7 +454,7 @@ argument sets to use for each test function. Let's run it:
E assert 1 == 2
test_parametrize.py:21: AssertionError
1 failed, 2 passed in 0.03s
1 failed, 2 passed in 0.12s
Indirect parametrization with multiple fixtures
--------------------------------------------------------------
@@ -475,11 +475,11 @@ Running it results in some skips if we don't have all the python interpreters in
.. code-block:: pytest
. $ pytest -rs -q multipython.py
ssssssssssss...ssssssssssss [100%]
ssssssssssssssssssssssss... [100%]
========================= short test summary info ==========================
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.7' not found
3 passed, 24 skipped in 0.24s
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.6' not found
3 passed, 24 skipped in 0.12s
Indirect parametrization of optional implementations/imports
--------------------------------------------------------------------

View File

@@ -436,7 +436,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
items = [1, 2, 3]
print("items is {!r}".format(items))
> a, b = items.pop()
E TypeError: 'int' object is not iterable
E TypeError: cannot unpack non-iterable int object
failure_demo.py:181: TypeError
--------------------------- Captured stdout call ---------------------------
@@ -516,7 +516,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
def test_z2_type_error(self):
items = 3
> a, b = items
E TypeError: 'int' object is not iterable
E TypeError: cannot unpack non-iterable int object
failure_demo.py:222: TypeError
______________________ TestMoreErrors.test_startswith ______________________

View File

@@ -65,7 +65,7 @@ Let's run this without supplying our new option:
test_sample.py:6: AssertionError
--------------------------- Captured stdout call ---------------------------
first
1 failed in 0.02s
1 failed in 0.12s
And now with supplying a command line option:
@@ -89,7 +89,7 @@ And now with supplying a command line option:
test_sample.py:6: AssertionError
--------------------------- Captured stdout call ---------------------------
second
1 failed in 0.02s
1 failed in 0.12s
You can see that the command line option arrived in our test. This
completes the basic pattern. However, one often rather wants to process
@@ -261,7 +261,7 @@ Let's run our little function:
E Failed: not configured: 42
test_checkconfig.py:11: Failed
1 failed in 0.02s
1 failed in 0.12s
If you only want to hide certain exceptions, you can set ``__tracebackhide__``
to a callable which gets the ``ExceptionInfo`` object. You can for example use

View File

@@ -81,4 +81,4 @@ If you run this without output capturing:
.test other
.test_unit1 method called
.
4 passed in 0.01s
4 passed in 0.12s

View File

@@ -301,6 +301,32 @@ are finalized when the last test of a *package* finishes.
Use this new feature sparingly and please make sure to report any issues you find.
Dynamic scope
^^^^^^^^^^^^^
In some cases, you might want to change the scope of the fixture without changing the code.
To do that, pass a callable to ``scope``. The callable must return a string with a valid scope
and will be executed only once - during the fixture definition. It will be called with two
keyword arguments - ``fixture_name`` as a string and ``config`` with a configuration object.
This can be especially useful when dealing with fixtures that need time for setup, like spawning
a docker container. You can use the command-line argument to control the scope of the spawned
containers for different environments. See the example below.
.. code-block:: python
def determine_scope(fixture_name, config):
if config.getoption("--keep-containers"):
return "session"
return "function"
@pytest.fixture(scope=determine_scope)
def docker_container():
yield spawn_container()
Order: Higher-scoped fixtures are instantiated first
----------------------------------------------------
@@ -361,7 +387,7 @@ Let's execute it:
$ pytest -s -q --tb=no
FFteardown smtp
2 failed in 0.79s
2 failed in 0.12s
We see that the ``smtp_connection`` instance is finalized after the two
tests finished execution. Note that if we decorated our fixture
@@ -515,7 +541,7 @@ again, nothing much has changed:
$ pytest -s -q --tb=no
FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)
2 failed in 0.77s
2 failed in 0.12s
Let's quickly create another test module that actually sets the
server URL in its module namespace:
@@ -692,7 +718,7 @@ So let's just do another run:
test_module.py:13: AssertionError
------------------------- Captured stdout teardown -------------------------
finalizing <smtplib.SMTP object at 0xdeadbeef>
4 failed in 1.69s
4 failed in 0.12s
We see that our two test functions each ran twice, against the different
``smtp_connection`` instances. Note also, that with the ``mail.python.org``
@@ -1043,7 +1069,7 @@ to verify our fixture is activated and the tests pass:
$ pytest -q
.. [100%]
2 passed in 0.01s
2 passed in 0.12s
You can specify multiple fixtures like this:
@@ -1151,7 +1177,7 @@ If we run it, we get two passing tests:
$ pytest -q
.. [100%]
2 passed in 0.01s
2 passed in 0.12s
Here is how autouse fixtures work in other scopes:

View File

@@ -28,7 +28,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py
This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.7/site-packages/pytest.py
.. _`simpletest`:
@@ -108,7 +108,7 @@ Execute the test function with “quiet” reporting mode:
$ pytest -q test_sysexit.py
. [100%]
1 passed in 0.01s
1 passed in 0.12s
Group multiple tests in a class
--------------------------------------------------------------
@@ -145,7 +145,7 @@ Once you develop multiple tests, you may want to group them into a class. pytest
E + where False = hasattr('hello', 'check')
test_class.py:8: AssertionError
1 failed, 1 passed in 0.02s
1 failed, 1 passed in 0.12s
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.
@@ -180,7 +180,7 @@ List the name ``tmpdir`` in the test function signature and ``pytest`` will look
test_tmpdir.py:3: AssertionError
--------------------------- Captured stdout call ---------------------------
PYTEST_TMPDIR/test_needsfiles0
1 failed in 0.02s
1 failed in 0.12s
More info on tmpdir handling is available at :ref:`Temporary directories and files <tmpdir handling>`.

View File

@@ -88,7 +88,7 @@ This has the following benefits:
.. note::
See :ref:`pythonpath` for more information about the difference between calling ``pytest`` and
See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and
``python -m pytest``.
Note that using this scheme your test files must have **unique names**, because

View File

@@ -161,7 +161,7 @@ the records for the ``setup`` and ``call`` stages during teardown like so:
yield window
for when in ("setup", "call"):
messages = [
x.message for x in caplog.get_records(when) if x.level == logging.WARNING
x.message for x in caplog.get_records(when) if x.levelno == logging.WARNING
]
if messages:
pytest.fail(

View File

@@ -205,7 +205,7 @@ If we now pass two stringinput values, our test will run twice:
$ pytest -q --stringinput="hello" --stringinput="world" test_strings.py
.. [100%]
2 passed in 0.01s
2 passed in 0.12s
Let's also run with a stringinput that will lead to a failing test:
@@ -225,7 +225,7 @@ Let's also run with a stringinput that will lead to a failing test:
E + where <built-in method isalpha of str object at 0xdeadbeef> = '!'.isalpha
test_strings.py:4: AssertionError
1 failed in 0.02s
1 failed in 0.12s
As expected our test function fails.
@@ -239,7 +239,7 @@ list:
s [100%]
========================= short test summary info ==========================
SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:2
1 skipped in 0.00s
1 skipped in 0.12s
Note that when calling ``metafunc.parametrize`` multiple times with different parameter sets, all parameter names across
those sets cannot be duplicated, otherwise an error will be raised.

View File

@@ -72,6 +72,8 @@ imported in the global import namespace.
This is also discussed in details in :ref:`test discovery`.
.. _`pytest vs python -m pytest`:
Invoking ``pytest`` versus ``python -m pytest``
-----------------------------------------------

View File

@@ -59,7 +59,7 @@ pytest.raises
**Tutorial**: :ref:`assertraises`.
.. autofunction:: pytest.raises(expected_exception: Exception, [match], [message])
.. autofunction:: pytest.raises(expected_exception: Exception, [match])
:with: excinfo
pytest.deprecated_call

View File

@@ -219,7 +219,7 @@ Running this test module ...:
$ pytest -q test_unittest_cleandir.py
. [100%]
1 passed in 0.01s
1 passed in 0.12s
... gives us one passed test because the ``initdir`` fixture function
was executed ahead of the ``test_method``.

View File

@@ -64,7 +64,7 @@ them into errors:
E UserWarning: api v1, should use functions from v2
test_show_warnings.py:5: UserWarning
1 failed in 0.02s
1 failed in 0.12s
The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option.
For example, the configuration below will ignore all user warnings, but will transform
@@ -407,7 +407,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta
class Test:
-- Docs: https://docs.pytest.org/en/latest/warnings.html
1 warnings in 0.00s
1 warnings in 0.12s
These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings.

View File

@@ -13,4 +13,5 @@ fi
python -m coverage combine
python -m coverage xml
python -m coverage report -m
bash <(curl -s https://codecov.io/bash) -Z -X gcov -X coveragepy -X search -X xcode -X gcovout -X fix -f coverage.xml
curl -S -L --retry 6 -s https://codecov.io/bash -o codecov-upload.sh
bash codecov-upload.sh -Z -X fix -f coverage.xml

View File

@@ -5,10 +5,15 @@ import traceback
from inspect import CO_VARARGS
from inspect import CO_VARKEYWORDS
from traceback import format_exception_only
from types import CodeType
from types import TracebackType
from typing import Any
from typing import Dict
from typing import Generic
from typing import List
from typing import Optional
from typing import Pattern
from typing import Set
from typing import Tuple
from typing import TypeVar
from typing import Union
@@ -29,7 +34,7 @@ if False: # TYPE_CHECKING
class Code:
""" wrapper around Python code objects """
def __init__(self, rawcode):
def __init__(self, rawcode) -> None:
if not hasattr(rawcode, "co_filename"):
rawcode = getrawcode(rawcode)
try:
@@ -38,7 +43,7 @@ class Code:
self.name = rawcode.co_name
except AttributeError:
raise TypeError("not a code object: {!r}".format(rawcode))
self.raw = rawcode
self.raw = rawcode # type: CodeType
def __eq__(self, other):
return self.raw == other.raw
@@ -351,7 +356,7 @@ class Traceback(list):
""" return the index of the frame/TracebackEntry where recursion
originates if appropriate, None if no recursion occurred
"""
cache = {}
cache = {} # type: Dict[Tuple[Any, int, int], List[Dict[str, Any]]]
for i, entry in enumerate(self):
# id for the code.raw is needed to work around
# the strange metaprogramming in the decorator lib from pypi
@@ -650,7 +655,7 @@ class FormattedExcinfo:
args.append((argname, saferepr(argvalue)))
return ReprFuncArgs(args)
def get_source(self, source, line_index=-1, excinfo=None, short=False):
def get_source(self, source, line_index=-1, excinfo=None, short=False) -> List[str]:
""" return formatted and marked up source lines. """
import _pytest._code
@@ -722,7 +727,7 @@ class FormattedExcinfo:
else:
line_index = entry.lineno - entry.getfirstlinesource()
lines = []
lines = [] # type: List[str]
style = entry._repr_style
if style is None:
style = self.style
@@ -799,7 +804,7 @@ class FormattedExcinfo:
exc_msg=str(e),
max_frames=max_frames,
total=len(traceback),
)
) # type: Optional[str]
traceback = traceback[:max_frames] + traceback[-max_frames:]
else:
if recursionindex is not None:
@@ -812,10 +817,12 @@ class FormattedExcinfo:
def repr_excinfo(self, excinfo):
repr_chain = []
repr_chain = (
[]
) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]]
e = excinfo.value
descr = None
seen = set()
seen = set() # type: Set[int]
while e is not None and id(e) not in seen:
seen.add(id(e))
if excinfo:
@@ -868,8 +875,8 @@ class TerminalRepr:
class ExceptionRepr(TerminalRepr):
def __init__(self):
self.sections = []
def __init__(self) -> None:
self.sections = [] # type: List[Tuple[str, str, str]]
def addsection(self, name, content, sep="-"):
self.sections.append((name, content, sep))

View File

@@ -7,6 +7,7 @@ import tokenize
import warnings
from ast import PyCF_ONLY_AST as _AST_FLAG
from bisect import bisect_right
from typing import List
import py
@@ -19,11 +20,11 @@ class Source:
_compilecounter = 0
def __init__(self, *parts, **kwargs):
self.lines = lines = []
self.lines = lines = [] # type: List[str]
de = kwargs.get("deindent", True)
for part in parts:
if not part:
partlines = []
partlines = [] # type: List[str]
elif isinstance(part, Source):
partlines = part.lines
elif isinstance(part, (tuple, list)):
@@ -157,8 +158,7 @@ class Source:
source = "\n".join(self.lines) + "\n"
try:
co = compile(source, filename, mode, flag)
except SyntaxError:
ex = sys.exc_info()[1]
except SyntaxError as ex:
# re-represent syntax errors from parsing python strings
msglines = self.lines[: ex.lineno]
if ex.offset:
@@ -173,7 +173,8 @@ class Source:
if flag & _AST_FLAG:
return co
lines = [(x + "\n") for x in self.lines]
linecache.cache[filename] = (1, None, lines, filename)
# Type ignored because linecache.cache is private.
linecache.cache[filename] = (1, None, lines, filename) # type: ignore
return co
@@ -282,7 +283,7 @@ def get_statement_startend2(lineno, node):
return start, end
def getstatementrange_ast(lineno, source, assertion=False, astnode=None):
def getstatementrange_ast(lineno, source: Source, assertion=False, astnode=None):
if astnode is None:
content = str(source)
# See #4260:

View File

@@ -2,6 +2,7 @@
support for presenting detailed information in failing assertions.
"""
import sys
from typing import Optional
from _pytest.assertion import rewrite
from _pytest.assertion import truncate
@@ -52,7 +53,9 @@ def register_assert_rewrite(*names):
importhook = hook
break
else:
importhook = DummyRewriteHook()
# TODO(typing): Add a protocol for mark_rewrite() and use it
# for importhook and for PytestPluginManager.rewrite_hook.
importhook = DummyRewriteHook() # type: ignore
importhook.mark_rewrite(*names)
@@ -69,7 +72,7 @@ class AssertionState:
def __init__(self, config, mode):
self.mode = mode
self.trace = config.trace.root.get("assertion")
self.hook = None
self.hook = None # type: Optional[rewrite.AssertionRewritingHook]
def install_importhook(config):
@@ -108,6 +111,7 @@ def pytest_runtest_setup(item):
"""
def callbinrepr(op, left, right):
# type: (str, object, object) -> Optional[str]
"""Call the pytest_assertrepr_compare hook and prepare the result
This uses the first result from the hook and then ensures the
@@ -133,12 +137,13 @@ def pytest_runtest_setup(item):
if item.config.getvalue("assertmode") == "rewrite":
res = res.replace("%", "%%")
return res
return None
util._reprcompare = callbinrepr
if item.ihook.pytest_assertion_pass.get_hookimpls():
def call_assertion_pass_hook(lineno, expl, orig):
def call_assertion_pass_hook(lineno, orig, expl):
item.ihook.pytest_assertion_pass(
item=item, lineno=lineno, orig=orig, expl=expl
)

View File

@@ -2,6 +2,7 @@
import ast
import errno
import functools
import importlib.abc
import importlib.machinery
import importlib.util
import io
@@ -16,6 +17,7 @@ from typing import Dict
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
import atomicwrites
@@ -34,7 +36,7 @@ PYC_EXT = ".py" + (__debug__ and "c" or "o")
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
class AssertionRewritingHook:
class AssertionRewritingHook(importlib.abc.MetaPathFinder):
"""PEP302/PEP451 import hook which rewrites asserts."""
def __init__(self, config):
@@ -44,13 +46,13 @@ class AssertionRewritingHook:
except ValueError:
self.fnpats = ["test_*.py", "*_test.py"]
self.session = None
self._rewritten_names = set()
self._must_rewrite = set()
self._rewritten_names = set() # type: Set[str]
self._must_rewrite = set() # type: Set[str]
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
# which might result in infinite recursion (#3506)
self._writing_pyc = False
self._basenames_to_check_rewrite = {"conftest"}
self._marked_for_rewrite_cache = {}
self._marked_for_rewrite_cache = {} # type: Dict[str, bool]
self._session_paths_checked = False
def set_session(self, session):
@@ -199,7 +201,7 @@ class AssertionRewritingHook:
return self._is_marked_for_rewrite(name, state)
def _is_marked_for_rewrite(self, name, state):
def _is_marked_for_rewrite(self, name: str, state):
try:
return self._marked_for_rewrite_cache[name]
except KeyError:
@@ -214,7 +216,7 @@ class AssertionRewritingHook:
self._marked_for_rewrite_cache[name] = False
return False
def mark_rewrite(self, *names):
def mark_rewrite(self, *names: str) -> None:
"""Mark import names as needing to be rewritten.
The named module or package as well as any nested modules will
@@ -381,6 +383,7 @@ def _format_boolop(explanations, is_or):
def _call_reprcompare(ops, results, expls, each_obj):
# type: (Tuple[str, ...], Tuple[bool, ...], Tuple[str, ...], Tuple[object, ...]) -> str
for i, res, expl in zip(range(len(ops)), results, expls):
try:
done = not res
@@ -396,11 +399,13 @@ def _call_reprcompare(ops, results, expls, each_obj):
def _call_assertion_pass(lineno, orig, expl):
# type: (int, str, str) -> None
if util._assertion_pass is not None:
util._assertion_pass(lineno=lineno, orig=orig, expl=expl)
util._assertion_pass(lineno, orig, expl)
def _check_if_assertion_pass_impl():
# type: () -> bool
"""Checks if any plugins implement the pytest_assertion_pass hook
in order not to generate explanation unecessarily (might be expensive)"""
return True if util._assertion_pass else False
@@ -574,7 +579,7 @@ class AssertionRewriter(ast.NodeVisitor):
def _assert_expr_to_lineno(self):
return _get_assertion_exprs(self.source)
def run(self, mod):
def run(self, mod: ast.Module) -> None:
"""Find all assert statements in *mod* and rewrite them."""
if not mod.body:
# Nothing to do.
@@ -616,12 +621,12 @@ class AssertionRewriter(ast.NodeVisitor):
]
mod.body[pos:pos] = imports
# Collect asserts.
nodes = [mod]
nodes = [mod] # type: List[ast.AST]
while nodes:
node = nodes.pop()
for name, field in ast.iter_fields(node):
if isinstance(field, list):
new = []
new = [] # type: List
for i, child in enumerate(field):
if isinstance(child, ast.Assert):
# Transform assert.
@@ -695,7 +700,7 @@ class AssertionRewriter(ast.NodeVisitor):
.explanation_param().
"""
self.explanation_specifiers = {}
self.explanation_specifiers = {} # type: Dict[str, ast.expr]
self.stack.append(self.explanation_specifiers)
def pop_format_context(self, expl_expr):
@@ -738,7 +743,8 @@ class AssertionRewriter(ast.NodeVisitor):
from _pytest.warning_types import PytestAssertRewriteWarning
import warnings
warnings.warn_explicit(
# Ignore type: typeshed bug https://github.com/python/typeshed/pull/3121
warnings.warn_explicit( # type: ignore
PytestAssertRewriteWarning(
"assertion is always true, perhaps remove parentheses?"
),
@@ -747,15 +753,15 @@ class AssertionRewriter(ast.NodeVisitor):
lineno=assert_.lineno,
)
self.statements = []
self.variables = []
self.statements = [] # type: List[ast.stmt]
self.variables = [] # type: List[str]
self.variable_counter = itertools.count()
if self.enable_assertion_pass_hook:
self.format_variables = []
self.format_variables = [] # type: List[str]
self.stack = []
self.expl_stmts = []
self.stack = [] # type: List[Dict[str, ast.expr]]
self.expl_stmts = [] # type: List[ast.stmt]
self.push_format_context()
# Rewrite assert into a bunch of statements.
top_condition, explanation = self.visit(assert_.test)
@@ -893,7 +899,7 @@ warn_explicit(
# Process each operand, short-circuiting if needed.
for i, v in enumerate(boolop.values):
if i:
fail_inner = []
fail_inner = [] # type: List[ast.stmt]
# cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
self.expl_stmts = fail_inner
@@ -904,10 +910,10 @@ warn_explicit(
call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call))
if i < levels:
cond = res
cond = res # type: ast.expr
if is_or:
cond = ast.UnaryOp(ast.Not(), cond)
inner = []
inner = [] # type: List[ast.stmt]
self.statements.append(ast.If(cond, inner, []))
self.statements = body = inner
self.statements = save
@@ -973,7 +979,7 @@ warn_explicit(
expl = pat % (res_expl, res_expl, value_expl, attr.attr)
return res, expl
def visit_Compare(self, comp):
def visit_Compare(self, comp: ast.Compare):
self.push_format_context()
left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
@@ -1006,7 +1012,7 @@ warn_explicit(
ast.Tuple(results, ast.Load()),
)
if len(comp.ops) > 1:
res = ast.BoolOp(ast.And(), load_names)
res = ast.BoolOp(ast.And(), load_names) # type: ast.expr
else:
res = load_names[0]
return res, self.explanation_param(self.pop_format_context(expl_call))

View File

@@ -1,6 +1,9 @@
"""Utilities for assertion debugging"""
import pprint
from collections.abc import Sequence
from typing import Callable
from typing import List
from typing import Optional
import _pytest._code
from _pytest import outcomes
@@ -10,11 +13,11 @@ from _pytest._io.saferepr import saferepr
# interpretation code and assertion rewriter to detect this plugin was
# loaded and in turn call the hooks defined here as part of the
# DebugInterpreter.
_reprcompare = None
_reprcompare = None # type: Optional[Callable[[str, object, object], Optional[str]]]
# Works similarly as _reprcompare attribute. Is populated with the hook call
# when pytest_runtest_setup is called.
_assertion_pass = None
_assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
def format_explanation(explanation):
@@ -177,7 +180,7 @@ def _diff_text(left, right, verbose=0):
"""
from difflib import ndiff
explanation = []
explanation = [] # type: List[str]
def escape_for_readable_diff(binary_text):
"""
@@ -235,7 +238,7 @@ def _compare_eq_verbose(left, right):
left_lines = repr(left).splitlines(keepends)
right_lines = repr(right).splitlines(keepends)
explanation = []
explanation = [] # type: List[str]
explanation += ["-" + line for line in left_lines]
explanation += ["+" + line for line in right_lines]
@@ -259,7 +262,7 @@ def _compare_eq_iterable(left, right, verbose=0):
def _compare_eq_sequence(left, right, verbose=0):
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
explanation = []
explanation = [] # type: List[str]
len_left = len(left)
len_right = len(right)
for i in range(min(len_left, len_right)):
@@ -327,7 +330,7 @@ def _compare_eq_set(left, right, verbose=0):
def _compare_eq_dict(left, right, verbose=0):
explanation = []
explanation = [] # type: List[str]
set_left = set(left)
set_right = set(right)
common = set_left.intersection(set_right)

View File

@@ -789,7 +789,11 @@ def _py36_windowsconsoleio_workaround(stream):
See https://github.com/pytest-dev/py/issues/103
"""
if not sys.platform.startswith("win32") or sys.version_info[:2] < (3, 6):
if (
not sys.platform.startswith("win32")
or sys.version_info[:2] < (3, 6)
or hasattr(sys, "pypy_version_info")
):
return
# bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666)

View File

@@ -9,6 +9,15 @@ import types
import warnings
from functools import lru_cache
from pathlib import Path
from types import TracebackType
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
import attr
import py
@@ -30,9 +39,12 @@ from _pytest._code import filter_traceback
from _pytest.compat import importlib_metadata
from _pytest.outcomes import fail
from _pytest.outcomes import Skipped
from _pytest.pathlib import unique_path
from _pytest.warning_types import PytestConfigWarning
if False: # TYPE_CHECKING
from typing import Type
hookimpl = HookimplMarker("pytest")
hookspec = HookspecMarker("pytest")
@@ -41,7 +53,7 @@ class ConftestImportFailure(Exception):
def __init__(self, path, excinfo):
Exception.__init__(self, path, excinfo)
self.path = path
self.excinfo = excinfo
self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType]
def main(args=None, plugins=None):
@@ -238,14 +250,18 @@ class PytestPluginManager(PluginManager):
def __init__(self):
super().__init__("pytest")
self._conftest_plugins = set()
# The objects are module objects, only used generically.
self._conftest_plugins = set() # type: Set[object]
# state related to local conftest plugins
self._dirpath2confmods = {}
self._conftestpath2mod = {}
# Maps a py.path.local to a list of module objects.
self._dirpath2confmods = {} # type: Dict[Any, List[object]]
# Maps a py.path.local to a module object.
self._conftestpath2mod = {} # type: Dict[Any, object]
self._confcutdir = None
self._noconftest = False
self._duplicatepaths = set()
# Set of py.path.local's.
self._duplicatepaths = set() # type: Set[Any]
self.add_hookspecs(_pytest.hookspec)
self.register(self)
@@ -367,7 +383,7 @@ class PytestPluginManager(PluginManager):
"""
current = py.path.local()
self._confcutdir = (
unique_path(current.join(namespace.confcutdir, abs=True))
current.join(namespace.confcutdir, abs=True)
if namespace.confcutdir
else None
)
@@ -406,13 +422,11 @@ class PytestPluginManager(PluginManager):
else:
directory = path
directory = unique_path(directory)
# XXX these days we may rather want to use config.rootdir
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir
clist = []
for parent in directory.parts():
for parent in directory.realpath().parts():
if self._confcutdir and self._confcutdir.relto(parent):
continue
conftestpath = parent.join("conftest.py")
@@ -432,12 +446,14 @@ class PytestPluginManager(PluginManager):
raise KeyError(name)
def _importconftest(self, conftestpath):
# Use realpath to avoid loading the same conftest twice
# Use a resolved Path object as key to avoid loading the same conftest twice
# with build systems that create build directories containing
# symlinks to actual files.
conftestpath = unique_path(conftestpath)
# Using Path().resolve() is better than py.path.realpath because
# it resolves to the correct path/drive in case-insensitive file systems (#5792)
key = Path(str(conftestpath)).resolve()
try:
return self._conftestpath2mod[conftestpath]
return self._conftestpath2mod[key]
except KeyError:
pkgpath = conftestpath.pypkgpath()
if pkgpath is None:
@@ -454,7 +470,7 @@ class PytestPluginManager(PluginManager):
raise ConftestImportFailure(conftestpath, sys.exc_info())
self._conftest_plugins.add(mod)
self._conftestpath2mod[conftestpath] = mod
self._conftestpath2mod[key] = mod
dirpath = conftestpath.dirpath()
if dirpath in self._dirpath2confmods:
for path, mods in self._dirpath2confmods.items():
@@ -657,7 +673,7 @@ class Config:
args = attr.ib()
plugins = attr.ib()
dir = attr.ib()
dir = attr.ib(type=Path)
def __init__(self, pluginmanager, *, invocation_params=None):
from .argparsing import Parser, FILE_OR_DIR
@@ -678,10 +694,10 @@ class Config:
self.pluginmanager = pluginmanager
self.trace = self.pluginmanager.trace.root.get("config")
self.hook = self.pluginmanager.hook
self._inicache = {}
self._override_ini = ()
self._opt2dest = {}
self._cleanup = []
self._inicache = {} # type: Dict[str, Any]
self._override_ini = () # type: Sequence[str]
self._opt2dest = {} # type: Dict[str, str]
self._cleanup = [] # type: List[Callable[[], None]]
self.pluginmanager.register(self, "pytestconfig")
self._configured = False
self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
@@ -782,7 +798,7 @@ class Config:
def pytest_load_initial_conftests(self, early_config):
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
def _initini(self, args):
def _initini(self, args) -> None:
ns, unknown_args = self._parser.parse_known_and_unknown_args(
args, namespace=copy.copy(self.option)
)
@@ -883,8 +899,7 @@ class Config:
self.hook.pytest_load_initial_conftests(
early_config=self, args=args, parser=self._parser
)
except ConftestImportFailure:
e = sys.exc_info()[1]
except ConftestImportFailure as e:
if ns.help or ns.version:
# we don't want to prevent --help/--version to work
# so just let is pass and print a warning at the end
@@ -950,7 +965,7 @@ class Config:
assert isinstance(x, list)
x.append(line) # modifies the cached list inline
def getini(self, name):
def getini(self, name: str):
""" return configuration value from an :ref:`ini file <inifiles>`. If the
specified name hasn't been registered through a prior
:py:func:`parser.addini <_pytest.config.Parser.addini>`
@@ -961,7 +976,7 @@ class Config:
self._inicache[name] = val = self._getini(name)
return val
def _getini(self, name):
def _getini(self, name: str) -> Any:
try:
description, type, default = self._parser._inidict[name]
except KeyError:
@@ -1006,7 +1021,7 @@ class Config:
values.append(relroot)
return values
def _get_override_ini_value(self, name):
def _get_override_ini_value(self, name: str) -> Optional[str]:
value = None
# override_ini is a list of "ini=value" options
# always use the last item if multiple values are set for same ini-name,
@@ -1021,7 +1036,7 @@ class Config:
value = user_ini_value
return value
def getoption(self, name, default=notset, skip=False):
def getoption(self, name: str, default=notset, skip: bool = False):
""" return command line option value.
:arg name: name of the option. You may also specify

View File

@@ -2,6 +2,11 @@ import argparse
import sys
import warnings
from gettext import gettext
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
import py
@@ -21,12 +26,12 @@ class Parser:
def __init__(self, usage=None, processopt=None):
self._anonymous = OptionGroup("custom options", parser=self)
self._groups = []
self._groups = [] # type: List[OptionGroup]
self._processopt = processopt
self._usage = usage
self._inidict = {}
self._ininames = []
self.extra_info = {}
self._inidict = {} # type: Dict[str, Tuple[str, Optional[str], Any]]
self._ininames = [] # type: List[str]
self.extra_info = {} # type: Dict[str, Any]
def processoption(self, option):
if self._processopt:
@@ -80,7 +85,7 @@ class Parser:
args = [str(x) if isinstance(x, py.path.local) else x for x in args]
return self.optparser.parse_args(args, namespace=namespace)
def _getparser(self):
def _getparser(self) -> "MyOptionParser":
from _pytest._argcomplete import filescompleter
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
@@ -94,7 +99,10 @@ class Parser:
a = option.attrs()
arggroup.add_argument(*n, **a)
# bash like autocompletion for dirs (appending '/')
optparser.add_argument(FILE_OR_DIR, nargs="*").completer = filescompleter
# Type ignored because typeshed doesn't know about argcomplete.
optparser.add_argument( # type: ignore
FILE_OR_DIR, nargs="*"
).completer = filescompleter
return optparser
def parse_setoption(self, args, option, namespace=None):
@@ -103,13 +111,15 @@ class Parser:
setattr(option, name, value)
return getattr(parsedoption, FILE_OR_DIR)
def parse_known_args(self, args, namespace=None):
def parse_known_args(self, args, namespace=None) -> argparse.Namespace:
"""parses and returns a namespace object with known arguments at this
point.
"""
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
def parse_known_and_unknown_args(self, args, namespace=None):
def parse_known_and_unknown_args(
self, args, namespace=None
) -> Tuple[argparse.Namespace, List[str]]:
"""parses and returns a namespace object with known arguments, and
the remaining arguments unknown at this point.
"""
@@ -163,8 +173,8 @@ class Argument:
def __init__(self, *names, **attrs):
"""store parms in private vars for use in add_argument"""
self._attrs = attrs
self._short_opts = []
self._long_opts = []
self._short_opts = [] # type: List[str]
self._long_opts = [] # type: List[str]
self.dest = attrs.get("dest")
if "%default" in (attrs.get("help") or ""):
warnings.warn(
@@ -268,8 +278,8 @@ class Argument:
)
self._long_opts.append(opt)
def __repr__(self):
args = []
def __repr__(self) -> str:
args = [] # type: List[str]
if self._short_opts:
args += ["_short_opts: " + repr(self._short_opts)]
if self._long_opts:
@@ -286,7 +296,7 @@ class OptionGroup:
def __init__(self, name, description="", parser=None):
self.name = name
self.description = description
self.options = []
self.options = [] # type: List[Argument]
self.parser = parser
def addoption(self, *optnames, **attrs):
@@ -405,6 +415,12 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
- cache result on action object as this is called at least 2 times
"""
def __init__(self, *args, **kwargs):
"""Use more accurate terminal width via pylib."""
if "width" not in kwargs:
kwargs["width"] = py.io.get_terminal_width()
super().__init__(*args, **kwargs)
def _format_action_invocation(self, action):
orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
if orgstr and orgstr[0] != "-": # only optional arguments
@@ -421,7 +437,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
option_map = getattr(action, "map_long_option", {})
if option_map is None:
option_map = {}
short_long = {}
short_long = {} # type: Dict[str, str]
for option in options:
if len(option) == 2 or option[2] == " ":
continue

View File

@@ -1,10 +1,15 @@
import os
from typing import List
from typing import Optional
import py
from .exceptions import UsageError
from _pytest.outcomes import fail
if False:
from . import Config # noqa: F401
def exists(path, ignore=EnvironmentError):
try:
@@ -102,7 +107,12 @@ def get_dirs_from_args(args):
CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None):
def determine_setup(
inifile: str,
args: List[str],
rootdir_cmd_arg: Optional[str] = None,
config: Optional["Config"] = None,
):
dirs = get_dirs_from_args(args)
if inifile:
iniconfig = py.iniconfig.IniConfig(inifile)

View File

@@ -29,3 +29,8 @@ RESULT_LOG = PytestDeprecationWarning(
"--result-log is deprecated and scheduled for removal in pytest 6.0.\n"
"See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information."
)
FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning(
"Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them "
"as a keyword argument instead."
)

View File

@@ -2,6 +2,7 @@ import functools
import inspect
import itertools
import sys
import warnings
from collections import defaultdict
from collections import deque
from collections import OrderedDict
@@ -27,6 +28,7 @@ from _pytest.compat import getlocation
from _pytest.compat import is_generator
from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
@@ -58,7 +60,6 @@ def pytest_sessionstart(session):
scopename2class = {} # type: Dict[str, Type[nodes.Node]]
scope2props = dict(session=()) # type: Dict[str, Tuple[str, ...]]
scope2props["package"] = ("fspath",)
scope2props["module"] = ("fspath", "module")
@@ -792,6 +793,25 @@ def _teardown_yield_fixture(fixturefunc, it):
)
def _eval_scope_callable(scope_callable, fixture_name, config):
try:
result = scope_callable(fixture_name=fixture_name, config=config)
except Exception:
raise TypeError(
"Error evaluating {} while defining fixture '{}'.\n"
"Expected a function with the signature (*, fixture_name, config)".format(
scope_callable, fixture_name
)
)
if not isinstance(result, str):
fail(
"Expected {} to return a 'str' while defining fixture '{}', but it returned:\n"
"{!r}".format(scope_callable, fixture_name, result),
pytrace=False,
)
return result
class FixtureDef:
""" A container for a factory definition. """
@@ -811,6 +831,8 @@ class FixtureDef:
self.has_location = baseid is not None
self.func = func
self.argname = argname
if callable(scope):
scope = _eval_scope_callable(scope, argname, fixturemanager.config)
self.scope = scope
self.scopenum = scope2index(
scope or "function",
@@ -995,7 +1017,57 @@ class FixtureFunctionMarker:
return function
def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
FIXTURE_ARGS_ORDER = ("scope", "params", "autouse", "ids", "name")
def _parse_fixture_args(callable_or_scope, *args, **kwargs):
arguments = {
"scope": "function",
"params": None,
"autouse": False,
"ids": None,
"name": None,
}
kwargs = {
key: value for key, value in kwargs.items() if arguments.get(key) != value
}
fixture_function = None
if isinstance(callable_or_scope, str):
args = list(args)
args.insert(0, callable_or_scope)
else:
fixture_function = callable_or_scope
positionals = set()
for positional, argument_name in zip(args, FIXTURE_ARGS_ORDER):
arguments[argument_name] = positional
positionals.add(argument_name)
duplicated_kwargs = {kwarg for kwarg in kwargs.keys() if kwarg in positionals}
if duplicated_kwargs:
raise TypeError(
"The fixture arguments are defined as positional and keyword: {}. "
"Use only keyword arguments.".format(", ".join(duplicated_kwargs))
)
if positionals:
warnings.warn(FIXTURE_POSITIONAL_ARGUMENTS, stacklevel=2)
arguments.update(kwargs)
return fixture_function, arguments
def fixture(
callable_or_scope=None,
*args,
scope="function",
params=None,
autouse=False,
ids=None,
name=None
):
"""Decorator to mark a fixture factory function.
This decorator can be used, with or without parameters, to define a
@@ -1041,21 +1113,55 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
``fixture_<fixturename>`` and then use
``@pytest.fixture(name='<fixturename>')``.
"""
if callable(scope) and params is None and autouse is False:
fixture_function, arguments = _parse_fixture_args(
callable_or_scope,
*args,
scope=scope,
params=params,
autouse=autouse,
ids=ids,
name=name
)
scope = arguments.get("scope")
params = arguments.get("params")
autouse = arguments.get("autouse")
ids = arguments.get("ids")
name = arguments.get("name")
if fixture_function and params is None and autouse is False:
# direct decoration
return FixtureFunctionMarker("function", params, autouse, name=name)(scope)
return FixtureFunctionMarker(scope, params, autouse, name=name)(
fixture_function
)
if params is not None and not isinstance(params, (list, tuple)):
params = list(params)
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=None):
def yield_fixture(
callable_or_scope=None,
*args,
scope="function",
params=None,
autouse=False,
ids=None,
name=None
):
""" (return a) decorator to mark a yield-fixture factory function.
.. deprecated:: 3.0
Use :py:func:`pytest.fixture` directly instead.
"""
return fixture(scope=scope, params=params, autouse=autouse, ids=ids, name=name)
return fixture(
callable_or_scope,
*args,
scope=scope,
params=params,
autouse=autouse,
ids=ids,
name=name
)
defaultfuncargprefixmarker = fixture()

View File

@@ -51,6 +51,8 @@ class MarkEvaluator:
except TEST_OUTCOME:
self.exc = sys.exc_info()
if isinstance(self.exc[1], SyntaxError):
# TODO: Investigate why SyntaxError.offset is Optional, and if it can be None here.
assert self.exc[1].offset is not None
msg = [" " * (self.exc[1].offset + 4) + "^"]
msg.append("SyntaxError: invalid syntax")
else:

View File

@@ -292,7 +292,7 @@ class MarkGenerator:
_config = None
_markers = set() # type: Set[str]
def __getattr__(self, name):
def __getattr__(self, name: str) -> MarkDecorator:
if name[0] == "_":
raise AttributeError("Marker name must NOT start with underscore")

View File

@@ -1,14 +1,26 @@
import os
import warnings
from functools import lru_cache
from typing import Any
from typing import Dict
from typing import List
from typing import Set
from typing import Tuple
from typing import Union
import py
import _pytest._code
from _pytest.compat import getfslineno
from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords
from _pytest.outcomes import fail
if False: # TYPE_CHECKING
# Imported here due to circular import.
from _pytest.fixtures import FixtureDef
SEP = "/"
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@@ -78,13 +90,13 @@ class Node:
self.keywords = NodeKeywords(self)
#: the marker objects belonging to this node
self.own_markers = []
self.own_markers = [] # type: List[Mark]
#: allow adding of extra keywords to use for matching
self.extra_keyword_matches = set()
self.extra_keyword_matches = set() # type: Set[str]
# used for storing artificial fixturedefs for direct parametrization
self._name2pseudofixturedef = {}
self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef]
if nodeid is not None:
assert "::()" not in nodeid
@@ -127,7 +139,8 @@ class Node:
)
)
path, lineno = get_fslocation_from_item(self)
warnings.warn_explicit(
# Type ignored: https://github.com/python/typeshed/pull/3121
warnings.warn_explicit( # type: ignore
warning,
category=None,
filename=str(path),
@@ -160,7 +173,9 @@ class Node:
chain.reverse()
return chain
def add_marker(self, marker, append=True):
def add_marker(
self, marker: Union[str, MarkDecorator], append: bool = True
) -> None:
"""dynamically add a marker object to the node.
:type marker: ``str`` or ``pytest.mark.*`` object
@@ -168,17 +183,19 @@ class Node:
``append=True`` whether to append the marker,
if ``False`` insert at position ``0``.
"""
from _pytest.mark import MarkDecorator, MARK_GEN
from _pytest.mark import MARK_GEN
if isinstance(marker, str):
marker = getattr(MARK_GEN, marker)
elif not isinstance(marker, MarkDecorator):
raise ValueError("is not a string or pytest.mark.* Marker")
self.keywords[marker.name] = marker
if append:
self.own_markers.append(marker.mark)
if isinstance(marker, MarkDecorator):
marker_ = marker
elif isinstance(marker, str):
marker_ = getattr(MARK_GEN, marker)
else:
self.own_markers.insert(0, marker.mark)
raise ValueError("is not a string or pytest.mark.* Marker")
self.keywords[marker_.name] = marker
if append:
self.own_markers.append(marker_.mark)
else:
self.own_markers.insert(0, marker_.mark)
def iter_markers(self, name=None):
"""
@@ -211,7 +228,7 @@ class Node:
def listextrakeywords(self):
""" Return a set of all extra keywords in self and any parents."""
extra_keywords = set()
extra_keywords = set() # type: Set[str]
for item in self.listchain():
extra_keywords.update(item.extra_keyword_matches)
return extra_keywords
@@ -239,13 +256,13 @@ class Node:
pass
def _repr_failure_py(self, excinfo, style=None):
if excinfo.errisinstance(fail.Exception):
# Type ignored: see comment where fail.Exception is defined.
if excinfo.errisinstance(fail.Exception): # type: ignore
if not excinfo.value.pytrace:
return str(excinfo.value)
fm = self.session._fixturemanager
if excinfo.errisinstance(fm.FixtureLookupError):
return excinfo.value.formatrepr()
tbfilter = True
if self.config.getoption("fulltrace", False):
style = "long"
else:
@@ -253,7 +270,6 @@ class Node:
self._prunetraceback(excinfo)
if len(excinfo.traceback) == 0:
excinfo.traceback = tb
tbfilter = False # prunetraceback already does it
if style == "auto":
style = "long"
# XXX should excinfo.getrepr record all data and toterminal() process it?
@@ -279,7 +295,7 @@ class Node:
abspath=abspath,
showlocals=self.config.getoption("showlocals", False),
style=style,
tbfilter=tbfilter,
tbfilter=False, # pruned already, or in --fulltrace mode.
truncate_locals=truncate_locals,
)
@@ -385,13 +401,13 @@ class Item(Node):
def __init__(self, name, parent=None, config=None, session=None, nodeid=None):
super().__init__(name, parent, config, session, nodeid=nodeid)
self._report_sections = []
self._report_sections = [] # type: List[Tuple[str, str, str]]
#: user properties is a list of tuples (name, value) that holds user
#: defined properties for this test.
self.user_properties = []
self.user_properties = [] # type: List[Tuple[str, Any]]
def add_report_section(self, when, key, content):
def add_report_section(self, when: str, key: str, content: str) -> None:
"""
Adds a new report section, similar to what's done internally to add stdout and
stderr captured output::

View File

@@ -59,20 +59,25 @@ def create_new_paste(contents):
Creates a new paste using bpaste.net service.
:contents: paste contents as utf-8 encoded bytes
:returns: url to the pasted contents
:returns: url to the pasted contents or error message
"""
import re
from urllib.request import urlopen
from urllib.parse import urlencode
params = {"code": contents, "lexer": "python3", "expiry": "1week"}
params = {"code": contents, "lexer": "text", "expiry": "1week"}
url = "https://bpaste.net"
response = urlopen(url, data=urlencode(params).encode("ascii")).read()
m = re.search(r'href="/raw/(\w+)"', response.decode("utf-8"))
try:
response = (
urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
)
except OSError as exc_info: # urllib errors
return "bad response: %s" % exc_info
m = re.search(r'href="/raw/(\w+)"', response)
if m:
return "{}/show/{}".format(url, m.group(1))
else:
return "bad response: " + response.decode("utf-8")
return "bad response: invalid format ('" + response + "')"
def pytest_terminal_summary(terminalreporter):

View File

@@ -11,7 +11,6 @@ from functools import partial
from os.path import expanduser
from os.path import expandvars
from os.path import isabs
from os.path import normcase
from os.path import sep
from posixpath import sep as posix_sep
@@ -335,12 +334,3 @@ def fnmatch_ex(pattern, path):
def parts(s):
parts = s.split(sep)
return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
def unique_path(path):
"""Returns a unique path in case-insensitive (but case-preserving) file
systems such as Windows.
This is needed only for ``py.path.local``; ``pathlib.Path`` handles this
natively with ``resolve()``."""
return type(path)(normcase(str(path.realpath())))

View File

@@ -1,5 +1,6 @@
from pprint import pprint
from typing import Optional
from typing import Union
import py
@@ -267,7 +268,8 @@ class TestReport(BaseReport):
if not isinstance(excinfo, ExceptionInfo):
outcome = "failed"
longrepr = excinfo
elif excinfo.errisinstance(skip.Exception):
# Type ignored -- see comment where skip.Exception is defined.
elif excinfo.errisinstance(skip.Exception): # type: ignore
outcome = "skipped"
r = excinfo._getreprcrash()
longrepr = (str(r.path), r.lineno, r.message)
@@ -431,7 +433,7 @@ def _report_kwargs_from_json(reportdict):
reprlocals=reprlocals,
filelocrepr=reprfileloc,
style=data["style"],
)
) # type: Union[ReprEntry, ReprEntryNative]
elif entry_type == "ReprEntryNative":
reprentry = ReprEntryNative(data["lines"])
else:

View File

@@ -3,6 +3,10 @@ import bdb
import os
import sys
from time import time
from typing import Callable
from typing import Dict
from typing import List
from typing import Tuple
import attr
@@ -10,10 +14,14 @@ from .reports import CollectErrorRepr
from .reports import CollectReport
from .reports import TestReport
from _pytest._code.code import ExceptionInfo
from _pytest.nodes import Node
from _pytest.outcomes import Exit
from _pytest.outcomes import Skipped
from _pytest.outcomes import TEST_OUTCOME
if False: # TYPE_CHECKING
from typing import Type
#
# pytest plugin hooks
@@ -99,8 +107,8 @@ def show_test_item(item):
tw = item.config.get_terminal_writer()
tw.line()
tw.write(" " * 8)
tw.write(item._nodeid)
used_fixtures = sorted(item._fixtureinfo.name2fixturedefs.keys())
tw.write(item.nodeid)
used_fixtures = sorted(getattr(item, "fixturenames", []))
if used_fixtures:
tw.write(" (fixtures used: {})".format(", ".join(used_fixtures)))
@@ -118,6 +126,7 @@ def pytest_runtest_call(item):
except Exception:
# Store trace info to allow postmortem debugging
type, value, tb = sys.exc_info()
assert tb is not None
tb = tb.tb_next # Skip *this* frame
sys.last_type = type
sys.last_value = value
@@ -185,7 +194,7 @@ def check_interactive_exception(call, report):
def call_runtest_hook(item, when, **kwds):
hookname = "pytest_runtest_" + when
ihook = getattr(item.ihook, hookname)
reraise = (Exit,)
reraise = (Exit,) # type: Tuple[Type[BaseException], ...]
if not item.config.getoption("usepdb", False):
reraise += (KeyboardInterrupt,)
return CallInfo.from_call(
@@ -252,7 +261,8 @@ def pytest_make_collect_report(collector):
skip_exceptions = [Skipped]
unittest = sys.modules.get("unittest")
if unittest is not None:
skip_exceptions.append(unittest.SkipTest)
# Type ignored because unittest is loaded dynamically.
skip_exceptions.append(unittest.SkipTest) # type: ignore
if call.excinfo.errisinstance(tuple(skip_exceptions)):
outcome = "skipped"
r = collector._repr_failure_py(call.excinfo, "line").reprcrash
@@ -266,7 +276,7 @@ def pytest_make_collect_report(collector):
rep = CollectReport(
collector.nodeid, outcome, longrepr, getattr(call, "result", None)
)
rep.call = call # see collect_one_node
rep.call = call # type: ignore # see collect_one_node
return rep
@@ -274,8 +284,8 @@ class SetupState:
""" shared state for setting up/tearing down test items or collectors. """
def __init__(self):
self.stack = []
self._finalizers = {}
self.stack = [] # type: List[Node]
self._finalizers = {} # type: Dict[Node, List[Callable[[], None]]]
def addfinalizer(self, finalizer, colitem):
""" attach a finalizer to the given colitem. """
@@ -302,6 +312,7 @@ class SetupState:
exc = sys.exc_info()
if exc:
_, val, tb = exc
assert val is not None
raise val.with_traceback(tb)
def _teardown_with_finalization(self, colitem):
@@ -335,6 +346,7 @@ class SetupState:
exc = sys.exc_info()
if exc:
_, val, tb = exc
assert val is not None
raise val.with_traceback(tb)
def prepare(self, colitem):

View File

@@ -399,6 +399,13 @@ def test_match_raises_error(testdir):
result = testdir.runpytest()
assert result.ret != 0
result.stdout.fnmatch_lines(["*AssertionError*Pattern*[123]*not found*"])
assert "__tracebackhide__ = True" not in result.stdout.str()
result = testdir.runpytest("--fulltrace")
assert result.ret != 0
result.stdout.fnmatch_lines(
["*__tracebackhide__ = True*", "*AssertionError*Pattern*[123]*not found*"]
)
class TestFormattedExcinfo:

View File

@@ -88,3 +88,30 @@ def tw_mock():
fullwidth = 80
return TWMock()
@pytest.fixture
def dummy_yaml_custom_test(testdir):
"""Writes a conftest file that collects and executes a dummy yaml test.
Taken from the docs, but stripped down to the bare minimum, useful for
tests which needs custom items collected.
"""
testdir.makeconftest(
"""
import pytest
def pytest_collect_file(parent, path):
if path.ext == ".yaml" and path.basename.startswith("test"):
return YamlFile(path, parent)
class YamlFile(pytest.File):
def collect(self):
yield YamlItem(self.fspath.basename, self)
class YamlItem(pytest.Item):
def runtest(self):
pass
"""
)
testdir.makefile(".yaml", test1="")

View File

@@ -2217,6 +2217,68 @@ class TestFixtureMarker:
["*ScopeMismatch*You tried*function*session*request*"]
)
def test_dynamic_scope(self, testdir):
testdir.makeconftest(
"""
import pytest
def pytest_addoption(parser):
parser.addoption("--extend-scope", action="store_true", default=False)
def dynamic_scope(fixture_name, config):
if config.getoption("--extend-scope"):
return "session"
return "function"
@pytest.fixture(scope=dynamic_scope)
def dynamic_fixture(calls=[]):
calls.append("call")
return len(calls)
"""
)
testdir.makepyfile(
"""
def test_first(dynamic_fixture):
assert dynamic_fixture == 1
def test_second(dynamic_fixture):
assert dynamic_fixture == 2
"""
)
reprec = testdir.inline_run()
reprec.assertoutcome(passed=2)
reprec = testdir.inline_run("--extend-scope")
reprec.assertoutcome(passed=1, failed=1)
def test_dynamic_scope_bad_return(self, testdir):
testdir.makepyfile(
"""
import pytest
def dynamic_scope(**_):
return "wrong-scope"
@pytest.fixture(scope=dynamic_scope)
def fixture():
pass
"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(
"Fixture 'fixture' from test_dynamic_scope_bad_return.py "
"got an unexpected scope value 'wrong-scope'"
)
def test_register_only_with_mark(self, testdir):
testdir.makeconftest(
"""
@@ -4044,12 +4106,43 @@ def test_fixture_named_request(testdir):
)
def test_fixture_duplicated_arguments(testdir):
"""Raise error if there are positional and keyword arguments for the same parameter (#1682)."""
with pytest.raises(TypeError) as excinfo:
@pytest.fixture("session", scope="session")
def arg(arg):
pass
assert (
str(excinfo.value)
== "The fixture arguments are defined as positional and keyword: scope. "
"Use only keyword arguments."
)
def test_fixture_with_positionals(testdir):
"""Raise warning, but the positionals should still works (#1682)."""
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
with pytest.warns(pytest.PytestDeprecationWarning) as warnings:
@pytest.fixture("function", [0], True)
def fixture_with_positionals():
pass
assert str(warnings[0].message) == str(FIXTURE_POSITIONAL_ARGUMENTS)
assert fixture_with_positionals._pytestfixturefunction.scope == "function"
assert fixture_with_positionals._pytestfixturefunction.params == (0,)
assert fixture_with_positionals._pytestfixturefunction.autouse
def test_indirect_fixture_does_not_break_scope(testdir):
"""Ensure that fixture scope is respected when using indirect fixtures (#570)"""
testdir.makepyfile(
"""
import pytest
instantiated = []
@pytest.fixture(scope="session")

View File

@@ -6,8 +6,8 @@ def mode(request):
return request.param
def test_show_only_active_fixtures(testdir, mode):
p = testdir.makepyfile(
def test_show_only_active_fixtures(testdir, mode, dummy_yaml_custom_test):
testdir.makepyfile(
'''
import pytest
@pytest.fixture
@@ -21,7 +21,7 @@ def test_show_only_active_fixtures(testdir, mode):
'''
)
result = testdir.runpytest(mode, p)
result = testdir.runpytest(mode)
assert result.ret == 0
result.stdout.fnmatch_lines(

View File

@@ -1,6 +1,6 @@
def test_show_fixtures_and_test(testdir):
def test_show_fixtures_and_test(testdir, dummy_yaml_custom_test):
""" Verifies that fixtures are not executed. """
p = testdir.makepyfile(
testdir.makepyfile(
"""
import pytest
@pytest.fixture
@@ -11,7 +11,7 @@ def test_show_fixtures_and_test(testdir):
"""
)
result = testdir.runpytest("--setup-plan", p)
result = testdir.runpytest("--setup-plan")
assert result.ret == 0
result.stdout.fnmatch_lines(

View File

@@ -1194,6 +1194,21 @@ def test_help_and_version_after_argument_error(testdir):
assert result.ret == ExitCode.USAGE_ERROR
def test_help_formatter_uses_py_get_terminal_width(testdir, monkeypatch):
from _pytest.config.argparsing import DropShorterLongHelpFormatter
monkeypatch.setenv("COLUMNS", "90")
formatter = DropShorterLongHelpFormatter("prog")
assert formatter._width == 90
monkeypatch.setattr("py.io.get_terminal_width", lambda: 160)
formatter = DropShorterLongHelpFormatter("prog")
assert formatter._width == 160
formatter = DropShorterLongHelpFormatter("prog", width=42)
assert formatter._width == 42
def test_config_does_not_load_blocked_plugin_from_args(testdir):
"""This tests that pytest's config setup handles "-p no:X"."""
p = testdir.makepyfile("def test(capfd): pass")

View File

@@ -1,12 +1,12 @@
import os.path
import os
import textwrap
from pathlib import Path
import py
import pytest
from _pytest.config import PytestPluginManager
from _pytest.main import ExitCode
from _pytest.pathlib import unique_path
def ConftestWithSetinitial(path):
@@ -143,11 +143,11 @@ def test_conftestcutdir(testdir):
# but we can still import a conftest directly
conftest._importconftest(conf)
values = conftest._getconftestmodules(conf.dirpath())
assert values[0].__file__.startswith(str(unique_path(conf)))
assert values[0].__file__.startswith(str(conf))
# and all sub paths get updated properly
values = conftest._getconftestmodules(p)
assert len(values) == 1
assert values[0].__file__.startswith(str(unique_path(conf)))
assert values[0].__file__.startswith(str(conf))
def test_conftestcutdir_inplace_considered(testdir):
@@ -156,7 +156,7 @@ def test_conftestcutdir_inplace_considered(testdir):
conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath())
values = conftest._getconftestmodules(conf.dirpath())
assert len(values) == 1
assert values[0].__file__.startswith(str(unique_path(conf)))
assert values[0].__file__.startswith(str(conf))
@pytest.mark.parametrize("name", "test tests whatever .dotdir".split())
@@ -165,11 +165,12 @@ def test_setinitial_conftest_subdirs(testdir, name):
subconftest = sub.ensure("conftest.py")
conftest = PytestPluginManager()
conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir)
key = Path(str(subconftest)).resolve()
if name not in ("whatever", ".dotdir"):
assert unique_path(subconftest) in conftest._conftestpath2mod
assert key in conftest._conftestpath2mod
assert len(conftest._conftestpath2mod) == 1
else:
assert subconftest not in conftest._conftestpath2mod
assert key not in conftest._conftestpath2mod
assert len(conftest._conftestpath2mod) == 0
@@ -282,7 +283,7 @@ def test_conftest_symlink_files(testdir):
reason="only relevant for case insensitive file systems",
)
def test_conftest_badcase(testdir):
"""Check conftest.py loading when directory casing is wrong."""
"""Check conftest.py loading when directory casing is wrong (#5792)."""
testdir.tmpdir.mkdir("JenkinsRoot").mkdir("test")
source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""}
testdir.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()})
@@ -292,6 +293,16 @@ def test_conftest_badcase(testdir):
assert result.ret == ExitCode.NO_TESTS_COLLECTED
def test_conftest_uppercase(testdir):
"""Check conftest.py whose qualified name contains uppercase characters (#5819)"""
source = {"__init__.py": "", "Foo/conftest.py": "", "Foo/__init__.py": ""}
testdir.makepyfile(**source)
testdir.tmpdir.chdir()
result = testdir.runpytest()
assert result.ret == ExitCode.NO_TESTS_COLLECTED
def test_no_conftest(testdir):
testdir.makeconftest("assert 0")
result = testdir.runpytest("--noconftest")

View File

@@ -1,6 +1,7 @@
import argparse
import distutils.spawn
import os
import shlex
import sys
import py
@@ -298,7 +299,11 @@ def test_argcomplete(testdir, monkeypatch):
# redirect output from argcomplete to stdin and stderr is not trivial
# http://stackoverflow.com/q/12589419/1307905
# so we use bash
fp.write('COMP_WORDBREAKS="$COMP_WORDBREAKS" python -m pytest 8>&1 9>&2')
fp.write(
'COMP_WORDBREAKS="$COMP_WORDBREAKS" {} -m pytest 8>&1 9>&2'.format(
shlex.quote(sys.executable)
)
)
# alternative would be exteneded Testdir.{run(),_run(),popen()} to be able
# to handle a keyword argument env that replaces os.environ in popen or
# extends the copy, advantage: could not forget to restore

View File

@@ -82,6 +82,47 @@ class TestPaste:
def pastebin(self, request):
return request.config.pluginmanager.getplugin("pastebin")
@pytest.fixture
def mocked_urlopen_fail(self, monkeypatch):
"""
monkeypatch the actual urlopen call to emulate a HTTP Error 400
"""
calls = []
import urllib.error
import urllib.request
def mocked(url, data):
calls.append((url, data))
raise urllib.error.HTTPError(url, 400, "Bad request", None, None)
monkeypatch.setattr(urllib.request, "urlopen", mocked)
return calls
@pytest.fixture
def mocked_urlopen_invalid(self, monkeypatch):
"""
monkeypatch the actual urlopen calls done by the internal plugin
function that connects to bpaste service, but return a url in an
unexpected format
"""
calls = []
def mocked(url, data):
calls.append((url, data))
class DummyFile:
def read(self):
# part of html of a normal response
return b'View <a href="/invalid/3c0c6750bd">raw</a>.'
return DummyFile()
import urllib.request
monkeypatch.setattr(urllib.request, "urlopen", mocked)
return calls
@pytest.fixture
def mocked_urlopen(self, monkeypatch):
"""
@@ -105,13 +146,26 @@ class TestPaste:
monkeypatch.setattr(urllib.request, "urlopen", mocked)
return calls
def test_pastebin_invalid_url(self, pastebin, mocked_urlopen_invalid):
result = pastebin.create_new_paste(b"full-paste-contents")
assert (
result
== "bad response: invalid format ('View <a href=\"/invalid/3c0c6750bd\">raw</a>.')"
)
assert len(mocked_urlopen_invalid) == 1
def test_pastebin_http_error(self, pastebin, mocked_urlopen_fail):
result = pastebin.create_new_paste(b"full-paste-contents")
assert result == "bad response: HTTP Error 400: Bad request"
assert len(mocked_urlopen_fail) == 1
def test_create_new_paste(self, pastebin, mocked_urlopen):
result = pastebin.create_new_paste(b"full-paste-contents")
assert result == "https://bpaste.net/show/3c0c6750bd"
assert len(mocked_urlopen) == 1
url, data = mocked_urlopen[0]
assert type(data) is bytes
lexer = "python3"
lexer = "text"
assert url == "https://bpaste.net"
assert "lexer=%s" % lexer in data.decode()
assert "code=full-paste-contents" in data.decode()
@@ -127,4 +181,4 @@ class TestPaste:
monkeypatch.setattr(urllib.request, "urlopen", response)
result = pastebin.create_new_paste(b"full-paste-contents")
assert result == "bad response: something bad occurred"
assert result == "bad response: invalid format ('something bad occurred')"

View File

@@ -853,7 +853,7 @@ class TestDebuggingBreakpoints:
Test that supports breakpoint global marks on Python 3.7+ and not on
CPython 3.5, 2.7
"""
if sys.version_info.major == 3 and sys.version_info.minor >= 7:
if sys.version_info >= (3, 7):
assert SUPPORTS_BREAKPOINT_BUILTIN is True
if sys.version_info.major == 3 and sys.version_info.minor == 5:
assert SUPPORTS_BREAKPOINT_BUILTIN is False

View File

@@ -77,7 +77,7 @@ commands =
[testenv:regen]
changedir = doc/en
skipsdist = True
basepython = python3.6
basepython = python3
deps =
sphinx
PyYAML
@@ -103,7 +103,7 @@ commands =
[testenv:release]
decription = do a release, required posarg of the version number
basepython = python3.6
basepython = python3
usedevelop = True
passenv = *
deps =
@@ -116,7 +116,7 @@ commands = python scripts/release.py {posargs}
[testenv:publish_gh_release_notes]
description = create GitHub release after deployment
basepython = python3.6
basepython = python3
usedevelop = True
passenv = GH_RELEASE_NOTES_TOKEN TRAVIS_TAG TRAVIS_REPO_SLUG
deps =