Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068ef90b92 | ||
|
|
065773aa97 | ||
|
|
b62276826c | ||
|
|
7bdfba3578 | ||
|
|
6bfd30d169 | ||
|
|
7731e45615 | ||
|
|
8806b1f531 | ||
|
|
19c9e53604 | ||
|
|
c28b63135f | ||
|
|
7c64d5d882 | ||
|
|
d3d9f9f668 | ||
|
|
018edf2a0e | ||
|
|
04c01fb606 | ||
|
|
ea0c7e43b6 | ||
|
|
de8fdab7a9 | ||
|
|
c1361b48f8 | ||
|
|
1b4ad7774b | ||
|
|
409cc2946a | ||
|
|
3114be9181 | ||
|
|
e4103cb02c | ||
|
|
217605c217 | ||
|
|
2fcf21a6c7 | ||
|
|
c8cf748c49 | ||
|
|
249b53e623 | ||
|
|
9669413b1f | ||
|
|
e2382e96ed | ||
|
|
1a9f4a51cb | ||
|
|
892bdd59dc | ||
|
|
df46afc96d | ||
|
|
6918d07560 | ||
|
|
c997c32004 | ||
|
|
450409d123 | ||
|
|
702acdba46 | ||
|
|
f832ac3316 | ||
|
|
9422e10322 | ||
|
|
5c3b4a6f52 | ||
|
|
05850d73bd | ||
|
|
b48f51eb03 | ||
|
|
cf5b544db3 | ||
|
|
73c5b7f4b1 | ||
|
|
8f2f51be6d | ||
|
|
f2f3ced508 | ||
|
|
23102a7d84 | ||
|
|
f0d538329c | ||
|
|
6c8bcf601c | ||
|
|
9d7b919c7d | ||
|
|
333e9d5c10 | ||
|
|
f1b605c95e | ||
|
|
2bb8d93001 | ||
|
|
d049b35397 | ||
|
|
8ee557f7ae | ||
|
|
ca3884d9bb | ||
|
|
bc163605ab | ||
|
|
1675048b35 | ||
|
|
10bf6aac76 | ||
|
|
f8dd6349c1 | ||
|
|
8c8809e1aa | ||
|
|
404cf0c872 | ||
|
|
d47b9d04d4 | ||
|
|
5bf9f9a711 | ||
|
|
c28e428249 | ||
|
|
c2f762460f | ||
|
|
f05ca74d27 | ||
|
|
2a6a1ca07d | ||
|
|
7259c453d6 | ||
|
|
28761c8da1 |
@@ -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
|
||||
|
||||
2
AUTHORS
2
AUTHORS
@@ -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ă
|
||||
|
||||
@@ -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)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -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@" \
|
||||
|
||||
@@ -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
|
||||
|
||||
23
doc/en/announce/release-5.1.3.rst
Normal file
23
doc/en/announce/release-5.1.3.rst
Normal 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
|
||||
36
doc/en/announce/release-5.2.0.rst
Normal file
36
doc/en/announce/release-5.2.0.rst
Normal 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
|
||||
@@ -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`:
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture("session")
|
||||
@pytest.fixture(scope="session")
|
||||
def setup(request):
|
||||
setup = CostlySetup()
|
||||
yield setup
|
||||
|
||||
@@ -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
|
||||
--------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
--------------------------------------------------------------------
|
||||
|
||||
@@ -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 ______________________
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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>`.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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``
|
||||
-----------------------------------------------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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``.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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::
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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())))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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="")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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
|
||||
|
||||
6
tox.ini
6
tox.ini
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user