Compare commits

...

296 Commits

Author SHA1 Message Date
pytest bot
b9c98762f5 Prepare release version 6.2.2 2021-01-25 12:30:53 +00:00
Bruno Oliveira
8003fd23b9 Merge pull request #8259 from nicoddemus/backport-8250
[6.2.x] Fix faulthandler for Twisted Logger when used with "--capture=no"
2021-01-20 10:14:34 -03:00
Bruno Oliveira
8d605b9b26 Merge pull request #8250 from daq-tools/fix-twisted-capture 2021-01-20 09:47:10 -03:00
Bruno Oliveira
14e0c3e105 Merge pull request #8225 from The-Compiler/training-update (#8226)
doc: Add note about training early bird discount
2021-01-05 20:49:50 +01:00
Bruno Oliveira
45facc16c8 Merge pull request #8224 from nicoddemus/backport-8220
[6.2.x] DOC: Mark pytest module
2021-01-05 13:31:19 -03:00
Bruno Oliveira
99fe887d7c Merge pull request #8220 from xuhdev/module-doc
DOC: Mark pytest module
2021-01-05 13:26:19 -03:00
Bruno Oliveira
8dbf9dc1aa Merge pull request #8167 from nicoddemus/backport-8166
[6.2.x] Add Changelog to setup.cfg (#8166)
2020-12-17 13:59:14 -03:00
Adam Johnson
baaee2148d Add Changelog to setup.cfg (#8166)
Co-authored-by: Thomas Grainger <tagrain@gmail.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-12-17 13:56:18 -03:00
Bruno Oliveira
f7d1ab870f Merge pull request #8163 from bluetech/backport-8152
[6.2.x] terminal: fix "(<Skipped instance>)" skip reason in test status line
2020-12-17 08:31:53 -03:00
Ran Benita
b8201c280e Merge pull request #8152 from bluetech/empty-skip
terminal: fix "(<Skipped instance>)" skip reason in test status line
(cherry picked from commit 02e69e5cdc)
2020-12-17 12:59:01 +02:00
Bruno Oliveira
1f0c50b475 Merge pull request #8160 from nicoddemus/backport-7381
[6.2.x] Clarify fixture execution order and provide visual aids (#7381)
2020-12-16 14:02:02 -03:00
Chris NeJame
da82e1853c Clarify fixture execution order and provide visual aids (#7381)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
Co-authored-by: Ran Benita <ran@unusedvar.com>
2020-12-16 13:54:52 -03:00
Bruno Oliveira
a566eb9c70 Merge pull request #8149 from pytest-dev/release-6.2.1
Prepare release 6.2.1
2020-12-15 12:39:06 -03:00
pytest bot
d3971c30f4 Prepare release version 6.2.1 2020-12-15 13:06:34 +00:00
Bruno Oliveira
780044b64a Merge pull request #8147 from nicoddemus/backport-8137
[6.2.x] python_api: handle array-like args in approx() #8137
2020-12-15 09:08:58 -03:00
Jakob van Santen
8354995abc python_api: handle array-like args in approx() (#8137) 2020-12-15 08:50:11 -03:00
Bruno Oliveira
8b8b1214f4 Merge pull request #8135 from nicoddemus/backport-8123
[6.2] Merge pull request #8123 from nicoddemus/import-mismatch-unc
2020-12-13 10:50:49 -03:00
Bruno Oliveira
f854cf66f4 Merge pull request #8123 from nicoddemus/import-mismatch-unc
Compare also paths on Windows when considering ImportPathMismatchError
2020-12-13 10:35:59 -03:00
Ran Benita
c475106f12 Merge pull request #8130 from pytest-dev/release-6.2.0
Prepare release 6.2.0
2020-12-12 23:21:28 +02:00
pytest bot
e7073afe6e Prepare release version 6.2.0 2020-12-12 22:45:09 +02:00
Ran Benita
683f29f84d Merge pull request #8129 from bluetech/docs-pygments-workaround
doc: temporary workaround for pytest-pygments lexing error
2020-12-12 22:31:10 +02:00
Ran Benita
0feeddf8ed doc: temporary workaround for pytest-pygments lexing error
pytest-pygments doesn't yet recognize the skip reason in summary line
added recently. Workaround it until we get to updating it.
2020-12-12 22:18:23 +02:00
Ran Benita
b478275777 Merge pull request #8128 from bluetech/skip-reason-empty
terminal: when the skip/xfail is empty, don't show it as "()"
2020-12-12 22:18:06 +02:00
Ran Benita
3302ff9949 terminal: when the skip/xfail is empty, don't show it as "()"
Avoid showing a line like

    x.py::test_4 XPASS ()   [100%]

which looks funny.
2020-12-12 22:09:00 +02:00
Ran Benita
59bd0f6912 Merge pull request #8126 from bluetech/tox-regen-pretend-scm2
tox: use pip legacy resolver for regen job
2020-12-12 20:50:10 +02:00
Ran Benita
6298ff1f4e tox: use pip legacy resolver for regen job
The env var effects all of the pip installs, including regendoc which
also uses setuptools-scm, so it gets the wrong version, and fails to
install with the new pip resolver:

    ERROR: Requested regendoc from 206e495142/regendoc-0.6.1.tar.gz (sha256)=db1e8c9ae02c1af559eae105bfd77ba41ed07fc8ca7030ea59db5f3f161236a4 has different version in metadata: '6.2.0'
2020-12-12 20:27:59 +02:00
Ran Benita
d51ecbd44d Merge pull request #8125 from bluetech/tox-rm-pip-req
tox: remove requires: pip>=20.3.1
2020-12-12 19:11:27 +02:00
Ran Benita
f237b077fc tox: remove requires: pip>=20.3.1
Causes some trouble in CI and not really needed as old pip should still
work.
2020-12-12 18:31:52 +02:00
Ran Benita
95e0e19b8d Merge pull request #8124 from bluetech/s0undt3ch-feature/skip-context-hook
Add `pytest_markeval_namespace` hook.
2020-12-12 18:14:02 +02:00
Anton
cf1051cfba infrastructure: Stricter tox dependensies (#8119) 2020-12-12 18:08:15 +02:00
Pedro Algarvio
b16c091253 Add pytest_markeval_namespace hook.
Add a new hook , `pytest_markeval_namespace` which should return a dictionary.
This dictionary will be used to augment the "global" variables available to evaluate skipif/xfail/xpass markers.

Pseudo example

``conftest.py``:

.. code-block:: python
   def pytest_markeval_namespace():
       return {"color": "red"}
``test_func.py``:

.. code-block:: python
   @pytest.mark.skipif("color == 'blue'", reason="Color is not red")
   def test_func():
       assert False
2020-12-12 17:41:37 +02:00
Ran Benita
902739cfc3 Merge pull request #7208 from CarycaKatarzyna/issue2044
Issue 2044 - show skipping reason in verbose mode
2020-12-09 10:11:47 +02:00
Katarzyna
612f157dbd Show reason for skipped test in verbose mode 2020-12-09 09:43:47 +02:00
Anton
810b878ef8 Migrate to pytester: test_capture.py, test_terminal.py, approx.py (#8108)
* Migrate to pytester: test_capture.py, test_config.py, approx.py

* migrate test_terminal.py

* revert test_config.py

* more typing in test_terminal.py

* try-out 'tr' fixture update

* revert 'tr' fixture, update test_config.py
2020-12-08 22:20:02 +02:00
Bruno Oliveira
059f6ff315 Merge pull request #8107 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-html-3.1.0
build(deps): bump pytest-html from 3.0.0 to 3.1.0 in /testing/plugins_integration
2020-12-07 08:52:56 -03:00
Bruno Oliveira
f3006ecb2b Merge pull request #8106 from pytest-dev/dependabot/pip/testing/plugins_integration/django-3.1.4
build(deps): bump django from 3.1.3 to 3.1.4 in /testing/plugins_integration
2020-12-07 08:52:36 -03:00
dependabot[bot]
19de6bccff build(deps): bump pytest-html in /testing/plugins_integration
Bumps [pytest-html](https://github.com/pytest-dev/pytest-html) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/pytest-dev/pytest-html/releases)
- [Changelog](https://github.com/pytest-dev/pytest-html/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-html/compare/v3.0.0...v3.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-07 03:08:29 +00:00
dependabot[bot]
8c120c042c build(deps): bump django in /testing/plugins_integration
Bumps [django](https://github.com/django/django) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.3...3.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-07 03:08:29 +00:00
Bruno Oliveira
68a0f22eee Merge pull request #8100 from nicoddemus/doctest-alternatives
List pytest-doctestplus in doctest docs
2020-12-05 17:42:09 -03:00
Ran Benita
1a1bb61340 Merge pull request #8038 from matthewhughes934/note-tmpdir-lifetime
Docs: Note lifetime of temporary directories
2020-12-05 21:54:04 +02:00
Ran Benita
e398c93884 Merge pull request #8055 from bluetech/unraisable
Add unraisableexception and threadexception plugins
2020-12-05 21:52:17 +02:00
Ran Benita
760a73c08c Merge pull request #8017 from bluetech/typing-public-fixtures
Export types of builtin fixtures for type annotations
2020-12-05 21:51:20 +02:00
Bruno Oliveira
4fc20c8d28 List pytest-doctestplus in doctest docs
As per https://github.com/pytest-dev/pytest/discussions/8088
2020-12-04 15:24:56 -03:00
Ran Benita
954151cdbd Merge pull request #8092 from cmecklenborg/pytester_refactor_test_pastebin
Migrate test_pastebin.py from testdir to pytester
2020-12-02 18:17:59 +02:00
Christine Mecklenborg
eeb3afb8ab Migrate test_pastebin.py from testdir to pytester 2020-12-01 12:55:59 -06:00
Ran Benita
64bb5f2ad1 Merge pull request #8091 from cmecklenborg/pytester_refactor_test_parseopt
Migrate test_parseopt.py from testdir to pytester
2020-12-01 14:10:09 +02:00
Ran Benita
0d0dfdd7aa Merge pull request #8090 from cmecklenborg/pytester_refactor_test_nose
Migrate test_nose.py from testdir to pytester
2020-12-01 14:07:55 +02:00
dependabot[bot]
7a06bc2416 build(deps): bump pytest-flakes in /testing/plugins_integration (#8087)
Bumps [pytest-flakes](https://github.com/asmeurer/pytest-flakes) from 4.0.2 to 4.0.3.
- [Release notes](https://github.com/asmeurer/pytest-flakes/releases)
- [Commits](https://github.com/asmeurer/pytest-flakes/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-01 14:06:38 +02:00
Christine Mecklenborg
4abd71121d Migrate test_parseopt.py from testdir to pytester 2020-11-30 16:27:39 -06:00
Christine Mecklenborg
d4c81ffab4 Migrate test_nose.py from testdir to pytester 2020-11-30 16:07:26 -06:00
Shubham Adep
d1cb9de211 Custom multiple marker execution order (#8065)
* Custom multiple marker execution order

https://github.com/pytest-dev/pytest/issues/8020 issue stated that ordering of multiple custom markers is from inside - out. I have added example for the same in the documentation. Please let me know for further changes / concerns.

* remove trailing spaces

The last commit was failing due to extra spaces

* Ran tox tests locally to debug white space trimming issues

* Resolve: ERROR:   docs: commands failed for tox -e docs

* Update doc/en/reference.rst

Committed PR suggestions.

Co-authored-by: Florian Bruhin <me@the-compiler.org>

* Added reference to Node.iter_markers_with_node in documentation

* Add myself to Authors

Co-authored-by: Shubham <shubham.adep@wsu.edu>
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2020-11-30 16:48:08 +01:00
Matthew Hughes
6a256606c6 Docs: Note lifetime of temporary directories
Explanation: The default handling of these lifetimes is done in
`tmpdir.TempPathFactory.getbasetemp`, which passes `keep=3` to
`pathlib.make_numbered_dir_with_cleanup`.

GH Issue: #8036
2020-11-28 21:18:16 +00:00
Prakhar Gurunani
3405c7e6a8 Add more info about skipping doctests (#8080)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-11-28 12:47:02 -03:00
Dominic Mortlock
775ba63c67 Refactor acceptance_test to use pytester (#8070) 2020-11-25 14:42:47 +02:00
Ran Benita
70823da7ed Merge pull request #8064 from symonk/fix-typo-in-mock-timing
fix mock_timing fixture name (typo) in timing.py
2020-11-25 12:29:34 +02:00
Jürgen Gmach
d27806295a fix typo (#8069) 2020-11-24 22:27:34 +01:00
Simon K
b310872300 fix mock_timing fixture name (typo) in timing.py 2020-11-23 20:45:12 +00:00
Ran Benita
d50df85e26 Add unraisableexception and threadexception plugins 2020-11-21 21:11:48 +02:00
Ran Benita
d59a4996ae Merge pull request #8057 from nicoddemus/changelog-links
Add links to some CHANGELOG entries
2020-11-21 20:47:27 +02:00
Maximilian Cosmo Sitter
0cef530d10 Add str() support to LineMatcher (#8050) 2020-11-21 20:45:20 +02:00
Bruno Oliveira
31021ac8d5 Add links to some CHANGELOG entries
While adding links to https://github.com/pytest-dev/pytest/pull/8052, noticed
a few more missing.
2020-11-21 11:05:54 -03:00
Simon K
52fef811c2 permit node to warn with any warning type, not just PytestWarning (#8052)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-11-21 10:49:17 -03:00
Ran Benita
148e3c582a pytester: always close stdin pipe in pytester.run()
If the user passed stdin=PIPE for some reason, they have no way to close
it themselves since it is not exposed.
2020-11-20 17:55:30 +02:00
Ran Benita
3e0bbd2f57 testing: fix ResourceWarning in broken-pipe test 2020-11-20 17:55:23 +02:00
Tim Hoffmann
afd53ede6f Link mentioned functions instead of using literals (#8045) 2020-11-19 15:44:59 +01:00
Bruno Oliveira
329e66c22e Merge pull request #8051 from The-Compiler/nose-fixup
Fix nose documentation
2020-11-19 10:04:59 -03:00
Florian Bruhin
c6ac618baf Fix nose documentation
Follow-up to #8048 which seems to have been merged without the suggested changes.
2020-11-19 13:54:40 +01:00
Bruno Oliveira
42f9622a90 Merge pull request #8048 from mickeypash/patch-1
Add small section on migrating from nose to pytest.
2020-11-19 09:02:04 -03:00
mickeypash
ce825ed16c Add small section on migrating from nose to pytest
The section currently features the nose2pytest tool with plans to expand
on some of the common gotchas when performing such migrations.
2020-11-19 08:50:11 -03:00
Petter Strandmark
eda681af2b Call Python 3.8 doClassCleanups (#8033) 2020-11-19 12:07:15 +02:00
Tim Hoffmann
b7ba76653d Prefix contextmanagers with module name in doc examples (#8044)
* Prefix contextmanagers with module name in doc examples

* Import pytest explicitly for doctests

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-11-19 12:06:24 +02:00
Ran Benita
30d89fd07e Merge pull request #8039 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-html-3.0.0
build(deps): bump pytest-html from 2.1.1 to 3.0.0 in /testing/plugins_integration
2020-11-19 12:05:04 +02:00
Ran Benita
8ea8cdb36d Merge pull request #8040 from symonk/remove-unused-var-in-pytester
stop assigning nextline if its potentially not used
2020-11-17 18:00:17 +02:00
symonk
3b677f79f4 stop assigning nextline if its potentially not used 2020-11-17 11:30:09 +00:00
Ran Benita
537215a16c Merge pull request #7987 from bluetech/ci-deprecated
ci: fix deprecation message & couple updates
2020-11-16 23:29:35 +02:00
Ran Benita
c2f949d68e Merge pull request #8013 from itsmegarvi/master
#7942 migrate from tempdir to pytester
2020-11-16 19:44:43 +02:00
Garvit Shubham
6fe9d2fb9f testing: convert test_{conftest,recwarn,tmpdir} to pytester 2020-11-16 19:22:57 +02:00
dependabot[bot]
3a899ced76 build(deps): bump pytest-html in /testing/plugins_integration
Bumps [pytest-html](https://github.com/pytest-dev/pytest-html) from 2.1.1 to 3.0.0.
- [Release notes](https://github.com/pytest-dev/pytest-html/releases)
- [Changelog](https://github.com/pytest-dev/pytest-html/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-html/compare/v2.1.1...v3.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-16 03:15:18 +00:00
Ran Benita
825b81ba52 Merge pull request #8014 from bluetech/pyc-pep552
assertion/rewrite: write pyc's according to PEP-552 on Python>=3.7
2020-11-14 23:38:45 +02:00
Ran Benita
1d532da49e assertion/rewrite: write pyc's according to PEP-552 on Python>=3.7
Python 3.7 changes the pyc format by adding a flags byte. Even though it
is not necessary for us to match it, it is nice to be able to read pyc
files we emit for debugging the rewriter.

Update our custom pyc files to use that format. We write flags==0
meaning we still use the mtime+size format rather the newer hash format.
2020-11-14 23:20:12 +02:00
Ran Benita
767cbeb086 Merge pull request #8002 from mcsitter/add-pythonenv-gitignore
Add pythonenv* to gitignore
2020-11-13 23:14:23 +02:00
Maximilian Cosmo Sitter
25e4dd0d2c Fix scope to accomodate requested changes 2020-11-13 20:00:44 +00:00
Ran Benita
c14f498622 Merge pull request #7908 from bluetech/post-release-tag
RELEASING: make sure we have an accurate version in a commit in master
2020-11-13 12:57:53 +02:00
Ran Benita
f6b682ad49 RELEASING: start new dev cycle by tagging MAJOR.{MINOR+1}.0.dev0 in master
This is needed so setuptools-scm in master shows an accurate version.
In particular, higher than the stable branch.
2020-11-13 12:38:58 +02:00
Ran Benita
701ff1f5a1 ci: only deploy to PyPI on X.Y.Z{,rcN} tags
We want to reserve other tags for our own purposes without it creating a
release.
2020-11-13 12:38:30 +02:00
Ran Benita
f1e6fdcddb Export types of builtin fixture for type annotations
In order to allow users to type annotate fixtures they request, the
types need to be imported from the `pytest` namespace. They are/were
always available to import from the `_pytest` namespace, but that is
not guaranteed to be stable.

These types are only exported for the purpose of typing. Specifically,
the following are *not* public:

- Construction (`__init__`)
- Subclassing
- staticmethods and classmethods

We try to combat them being used anyway by:

- Marking the classes as `@final` when possible (already done).

- Not documenting private stuff in the API Reference.

- Using `_`-prefixed names or marking as `:meta private:` for private
  stuff.

- Adding a keyword-only `_ispytest=False` to private constructors,
  warning if False, and changing pytest itself to pass True. In the
  future it will (hopefully) become a hard error.

Hopefully that will be enough.
2020-11-13 11:25:09 +02:00
Ran Benita
b050578882 pytester: split asserts to a separate plugin, don't rewrite pytester itself
An upcoming commit wants to import from `_pytest.pytester` in the public
`pytest` module. This means that `_pytest.pytester` would start to get
imported during import time, which it hasn't up to now -- it was
imported by the plugin loader (if requested). When a plugin is loaded,
it is subjected to assertion rewriting, but only if the module isn't
imported yet, it issues a warning "Module already imported so cannot be
rewritten" and skips the rewriting. So we'd end up with the pytester
plugin not being rewritten, but it wants to be.

Absent better ideas, the solution here is to split the pytester
assertions to their own plugin (which will always only be imported by
the plugin loader) and exclude pytester itself from plugin rewriting.
2020-11-13 11:25:09 +02:00
Ran Benita
66311ff702 Merge pull request #8022 from bluetech/doctest-init
main: fix only one doctest collected on pytest --doctest-modules __init__.py
2020-11-13 10:46:46 +02:00
Ran Benita
ea3c0aa245 Merge pull request #8019 from JosiasAurel/mypytester-change-01
Migrate from testdir to pytester
2020-11-13 10:36:03 +02:00
Bruno Oliveira
843bca8c0c Merge pull request #8025 from adamchainz/issue_8023_norecursedirs
Add 'node_modules' to norecursedirs
2020-11-12 07:36:38 -03:00
Josias Aurel
fa148eadfe Update testing/test_faulthandler.py
Co-authored-by: Sanket Duthade <duthades@gmail.com>
2020-11-11 18:35:06 +01:00
Bruno Oliveira
ff9e35243e Merge pull request #7928 from graingert/feature-request-template 2020-11-11 09:20:00 -03:00
Josias Aurel
06a597db14 Add type annotations 2020-11-11 05:02:32 +01:00
Josias Aurel
1ed8159c7d Update testing/test_faulthandler.py
Co-authored-by: Sanket Duthade <duthades@gmail.com>
2020-11-11 04:45:57 +01:00
Josias Aurel
8320c07134 Update testing/test_faulthandler.py
Co-authored-by: Sanket Duthade <duthades@gmail.com>
2020-11-11 04:45:42 +01:00
Adam Johnson
39b2706f6a Add 'node_modules' to norecursedirs
Fixes #8023.
2020-11-11 01:52:18 +00:00
Ran Benita
265cc2cfec main: fix only one doctest collected on pytest --doctest-modules __init__.py
When --doctest-modules is used, an `__init__.py` file is not a `Package`
but a `DoctestModule`, but some collection code assumed that
`__init__.py` implies a `Package`. That code caused only a single test
to be collected in the scenario in the subject.

Tighten up this check to explicitly check for `Package`. There are
better solutions, but for another time.

Report & test by Nick Gates <nickgatzgates@gmail.com>.
2020-11-10 22:50:46 +02:00
Josias Aurel
043ed55056 Migrate from testdir to pytester 2020-11-09 18:07:34 +01:00
Ran Benita
e986d84466 Merge pull request #8006 from bluetech/export-MonkeyPatch
Export MonkeyPatch as pytest.MonkeyPatch
2020-11-09 11:45:38 +02:00
Ran Benita
6f13d1b03b Export MonkeyPatch as pytest.MonkeyPatch
We want to export `pytest.MonkeyPatch` for the purpose of
type-annotating the `monkeypatch` fixture. For other fixtures we export
in this way, we also make direct construction of them (e.g.
`MonkeyPatch()`) private. But unlike the others, `MonkeyPatch` is also
widely used directly already, mostly because the `monkeypatch` fixture
only works in `function` scope (issue #363), but also in other cases. So
making it private will be annoying and we don't offer a decent
replacement yet.

So, let's just make direct construction public & documented.
2020-11-09 11:28:15 +02:00
Ran Benita
7aa5e49fc4 Merge pull request #8012 from pytest-dev/dependabot/pip/testing/plugins_integration/django-3.1.3
build(deps): bump django from 3.1.2 to 3.1.3 in /testing/plugins_integration
2020-11-09 11:26:35 +02:00
dependabot[bot]
02d4b3d75f build(deps): bump django in /testing/plugins_integration
Bumps [django](https://github.com/django/django) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.2...3.1.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-09 03:17:35 +00:00
Bruno Oliveira
b2e7b9df9e Merge pull request #8010 from nicoddemus/pluralize 2020-11-08 13:31:27 -03:00
Bruno Oliveira
c7f8ad17f5 Rename _make_plural to pluralize
A bit shorter and a better name, IMHO.
2020-11-08 12:42:52 -03:00
Hugo Martins
5b2e5e8a40 Improve summary stats when using '--collect-only' (#7875)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-11-08 11:45:10 -03:00
Ran Benita
29f2f4e854 Merge pull request #8005 from bluetech/pytest-import
Stop importing `pytest` to avoid upcoming import cycles
2020-11-08 14:15:19 +02:00
Bruno Oliveira
10a3a49bd6 Merge pull request #8007 from bluetech/dont-ignore-already-imported
testing: don't ignore "Module already imported so cannot be rewritten" warning
2020-11-07 22:46:15 -03:00
frankgerhardt
9bc633064b Capitalize headlines (#8008) 2020-11-07 22:44:04 -03:00
Ran Benita
361f9e20c3 testing: don't ignore "Module already imported so cannot be rewritten" warning
The test suite passes without it being ignored. The absence of this
warning cost me some head-scratching time, so enable it again.
2020-11-07 18:14:21 +02:00
Ran Benita
1cbb0c3554 Stop importing pytest to avoid upcoming import cycles
Don't import `pytest` from within some `_pytest` modules since an
upcoming commit will import from them into `pytest`.

It would have been nice not to have to do it, so that internal plugins
look more like external plugins, but with the existing layout this seems
unavoidable.
2020-11-07 18:08:30 +02:00
Ran Benita
c784c142a4 Merge pull request #7988 from bluetech/deprecate-yield-fixture
fixtures: deprecate pytest.yield_fixture()
2020-11-07 18:07:00 +02:00
Ran Benita
4c0513bc18 fixtures: deprecate pytest.yield_fixture() 2020-11-07 17:06:40 +02:00
Sanket Duthade
3bcd316f07 test_collection.py migrate from testdir to Pytester (#8003) 2020-11-07 16:56:00 +02:00
Garvit Shubham
6a5037a25b #7942 test_setupplan.py migrate from testdir to Pytester (#8004)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-11-07 09:29:45 -03:00
Maximilian Cosmo Sitter
a73fb6e006 Add pythonenv* to gitignore 2020-11-06 14:29:12 +00:00
Bruno Oliveira
30287b49cd Deprecate --strict (#7985)
Fix #7530
2020-11-06 09:48:20 +01:00
Anthony Sottile
4cd0fde277 Merge pull request #8001 from bluetech/silence-pyparsing-warning
testing: silence deprecation warning from older pyparsing releases
2020-11-05 08:46:50 -08:00
Ran Benita
070f8e0f9d testing: silence deprecation warning from older pyparsing releases
This causes some tests to fail when using these older versions.
2020-11-05 16:08:54 +02:00
Ran Benita
1d4cc7eb36 Merge pull request #7997 from duthades/master
#7942 test_session.py migrate from testdir to Pytester
2020-11-05 15:55:36 +02:00
duthades
b815f430e5 #7942 test_session.py migrate from testdir to pytester
- Add name to AUTHORS
2020-11-04 21:55:07 +05:30
Ran Benita
3adece9fb7 Merge pull request #7990 from bluetech/xunit-quadratic-3
unittest: fix quadratic behavior in collection of unittests using setUpClass/setup_method
2020-11-03 18:01:25 +02:00
Ran Benita
489f6f4499 unittest: fix quadratic behavior in collection of unittests using setUpClass/setup_method
This is similar to 50114d4731876dae; I missed that unittest does the
same thing.
2020-11-01 15:17:42 +02:00
Ran Benita
a95da7a425 Merge pull request #7980 from bluetech/code-changes
code: a few minor improvements
2020-11-01 09:51:39 +02:00
Ran Benita
8aa9ea95e1 ci: test on Python 3.9 final 2020-11-01 09:49:41 +02:00
Ran Benita
76226182ae ci: change cache action to v2
Supposed to be faster.
2020-11-01 09:49:41 +02:00
Ran Benita
f9d82a34f4 ci: replace deprecated ::set-env 2020-11-01 09:49:23 +02:00
Ran Benita
7fb0ea3f68 Merge pull request #7956 from csernazs/fix-7951
Fix handling recursive symlinks
2020-10-31 18:59:50 +02:00
Cserna Zsolt
8a38e7a6e8 Fix handling recursive symlinks
When pytest was run on a directory containing a recursive symlink it failed
with ELOOP as the library was not able to determine the type of the
direntry:

src/_pytest/main.py:685: in collect
    if not direntry.is_file():
E   OSError: [Errno 40] Too many levels of symbolic links: '/home/florian/proj/pytest/tests/recursive'

This is fixed by handling ELOOP and other errors in the visit function in
pathlib.py, so the entries whose is_file() call raises an OSError with the
pre-defined list of error numbers will be exluded from the result.

The _ignore_errors function was copied from Lib/pathlib.py of cpython 3.9.

Fixes #7951
2020-10-31 17:40:56 +01:00
Ran Benita
1c18fb8ccc Merge pull request #7553 from tirkarthi/namedtuple-diff
Add support to display field names in namedtuple diffs.
2020-10-31 15:02:31 +02:00
Bruno Oliveira
2753859ff0 Merge pull request #7979 from nicoddemus/metafunc-ref
Add FunctionDefinition to the reference docs
2020-10-31 10:00:48 -03:00
Ran Benita
a14a229d1b Merge pull request #7982 from bluetech/symlink-collect
pathlib: fix symlinked directories not followed during collection
2020-10-31 14:46:50 +02:00
Karthikeyan Singaravelan
9a0f4e57ee Add support to display field names in namedtuple diffs. 2020-10-31 14:41:53 +02:00
Ran Benita
dd323980f9 Merge pull request #7978 from nicoddemus/port-4.6-release-notes
Manually add the remaining 4.6.x release notes to the changelog
2020-10-31 14:37:45 +02:00
Ran Benita
6cdae8ed40 pathlib: fix symlinked directories not followed during collection 2020-10-31 14:22:15 +02:00
Bruno Oliveira
569c091769 Add FunctionDefinition to the reference docs
Fix #7968
2020-10-31 08:45:34 -03:00
Bruno Oliveira
0c7233032f Manually add the remaining 4.6.x release notes to the changelog
Fix #7967
2020-10-31 08:36:26 -03:00
Ran Benita
531416cc5a code: simplify Code construction 2020-10-31 12:40:25 +02:00
Ran Benita
6506f016ac testing/test_source: use unqualified imports 2020-10-31 12:40:25 +02:00
Ran Benita
a1df458e85 code: use properties for derived attributes, use slots
Make the objects more light weight.

Remove unused properties.
2020-10-31 12:40:25 +02:00
Ariel Pillemer
a7e38c5c61 pytest-dev#7942 test_runner_xunit.py (#7964) 2020-10-31 12:38:11 +02:00
crricks
3c7eb5a398 migrated test_nodes.py from testdir to pytester #7492. (#7969) 2020-10-30 22:34:05 +02:00
Ran Benita
ad94456ca0 Merge pull request #7976 from symonk/7942-refactor-stepwise-to-use-pytester
#7942 refactor stepwise tests to utilize pytester
2020-10-30 22:15:12 +02:00
Christine Mecklenborg
aa843746a4 Migrate test_error_diffs.py from testdir to pytester (#7971) 2020-10-30 22:12:40 +02:00
symonk
c58abf7ad1 #7942 refactor stepwise tests to utilize pytester 2020-10-30 19:21:42 +00:00
Ran Benita
5913cd20ec assertion/util: remove unhelpful type_fns indirection
It doesn't serve any purpose that I am able to discern.
2020-10-30 21:15:48 +02:00
Simon K
6cddeb8cb3 #7938 - [Plugin: Stepwise][Enhancements] Refactoring, smarter registration & --sw-skip functionality (#7939)
* adding --sw-skip shorthand for stepwise skip

* be explicit rather than implicit with default args for stepwise

* add constant for sw cache dir; only register plugin if necessary rather check check activity always;

* use str format; remove unused args in hooks

* assert cache upfront, allow stepwise to have a reference to the cache

* type hinting lf, skip, move literal strings into module constants

* convert parametrized option into a list

* add a sessionfinish hook for stepwise to keep backwards behaviour the same

* add changelog for #7938

* Improve performance of stepwise modifyitems & address PR feedback

* add test for stepwise deselected based on performance enhancements

* Apply suggestions from code review

* delete from items, account for edge case where failed_index = 0

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-10-30 19:13:06 +00:00
Ran Benita
0cd190f037 Merge pull request #7970 from cmecklenborg/pytester_refactor_test_assertion
Migrate test_assertion.py from testdir to pytester
2020-10-30 20:56:21 +02:00
Christine Mecklenborg
47ff911c8f Migrate test_assertion.py from testdir to pytester 2020-10-29 20:39:44 -05:00
Ran Benita
8f52fc777a Merge pull request #7962 from bluetech/mypy-typed-deps
pre-commit: install typed dependencies in the mypy target
2020-10-29 17:52:12 +02:00
Ran Benita
65dfa98877 Merge pull request #7961 from bluetech/rm-checkqa-mypy
tox: remove checkqa-mypy environment
2020-10-29 17:25:51 +02:00
Christine Mecklenborg
65148e3120 Migrate test_compat.py from testdir to pytester (#7963) 2020-10-29 09:56:34 +02:00
Christine Mecklenborg
460b51dd95 Migrate test_setuponly.py from testdir to pytester (#7959) 2020-10-29 09:55:30 +02:00
Christine Mecklenborg
efe470bf1c Migrate test_assertrewrite.py from testdir to pytester (#7952) 2020-10-29 09:54:34 +02:00
Ran Benita
e3ce5d6b53 pre-commit: install typed dependencies in the mypy target
Otherwise, mypy doesn't know about them and their types are considered
Any.
2020-10-28 22:22:33 +02:00
Ran Benita
de810152ec tox: remove checkqa-mypy environment
We run mypy through pre-commit, and we don't keep duplicate targets in
tox for all of the other linters. Since this adds some (small)
maintenance overhead, remove it.
2020-10-28 22:16:25 +02:00
Bruno Oliveira
b95991aeea Merge pull request #7960 from nicoddemus/cherry-pick-release
Merge pull request #7958 from pytest-dev/release-6.1.2
2020-10-28 14:36:37 -03:00
Bruno Oliveira
5711ced250 Merge pull request #7958 from pytest-dev/release-6.1.2
Prepare release 6.1.2

(cherry picked from commit 1ed903e8fc)
2020-10-28 14:23:09 -03:00
Christine M
8d369f73ba Migrate test_skipping.py from testdir to pytester (#7953) 2020-10-28 17:05:54 +02:00
Ran Benita
6cd6d9b61a Merge pull request #7949 from gcamargo1/gcamargo1-patch-1
Update doctest.rst
2020-10-28 15:56:59 +02:00
Vasilis Gerakaris
a431310c0a Increase temp dir deletion period to 3 days (#7914)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-10-28 08:23:35 -03:00
Ran Benita
78c09b9931 Merge pull request #7944 from symonk/refactor-test-mark-to-use-pytester
Refactor test_warning_types.py & test_mark.py to use pytester
2020-10-27 21:06:42 +02:00
symonk
434e30424e Address feedback for converting testdir to pytester 2020-10-27 17:50:54 +00:00
Gustavo Camargo
b308c6ddb3 Update doctest.rst 2020-10-26 14:30:00 -05:00
Ran Benita
0fe8a8dfe6 Merge pull request #7945 from proggga/fix_test_helpconfig_7942
#7942 test_helpconfig.py migrate from testdir to pytester
2020-10-26 17:52:41 +02:00
Mikhail Fesenko
cd9b3618c7 #7942 test_helpconfig.py migrate from testdir to pytester 2020-10-26 18:30:48 +03:00
Bruno Oliveira
20b710c4b4 Merge pull request #7948 from bluetech/testing-conftest-pytester
testing: make conftest stuff check for pytester not testdir
2020-10-26 12:08:19 -03:00
Ran Benita
c31f4dc112 testing: make conftest stuff check for pytester not testdir
testdir uses pytester so this applies to it as well, but now includes
pytester as well.
2020-10-26 15:01:38 +02:00
Ran Benita
096d096539 Merge pull request #7946 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-django-4.1.0
build(deps): bump pytest-django from 4.0.0 to 4.1.0 in /testing/plugins_integration
2020-10-26 14:36:49 +02:00
dependabot[bot]
f7c5067823 build(deps): bump pytest-django in /testing/plugins_integration
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.0.0...v4.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-26 03:15:50 +00:00
symonk
cde50db6e7 add type hint to parametrized warning_class 2020-10-25 18:31:43 +00:00
symonk
c818ac2248 Tidy up type hints for pytest in test_marks & test_warning_types 2020-10-25 18:03:59 +00:00
symonk
6b7203aba7 add conversion for test_warning_types.py also 2020-10-25 17:38:12 +00:00
symonk
7495d2c345 add missing pytester type hints 2020-10-25 17:33:40 +00:00
symonk
1bd83e75a4 refactor test mark to use new pytester 2020-10-25 17:27:19 +00:00
Ran Benita
8105e60f20 Merge pull request #7937 from bluetech/testing-fixes
testing: fix pexpect hang
2020-10-25 16:53:03 +02:00
Ran Benita
ca82214444 pytester: workaround issue causing spawn to crash or hang
In pytester tests, pytest stashes & restores the sys.modules for each
test. So if the test imports a new module, it is initialized anew each
time.

Turns out the readline module isn't multi-init safe, which causes
pytester.spawn to crash or hang. So preserve it as a workaround.
2020-10-25 16:31:47 +02:00
Ran Benita
897f151e94 testing: use pytester.spawn instead of testdir
Part of investigating a bug, but didn't fix it.
2020-10-25 10:11:10 +02:00
Ran Benita
25dee8fef6 testing: fix test_assertrewrite with PYTHONPYCACHEPREFIX
Make the tests work when running with PYTHONPYCACHEPREFIX (possible when
running in a dirty environment, not under tox).
2020-10-25 10:11:10 +02:00
Ran Benita
d9ac2efbcd testing: python 3.10 fix 2020-10-25 01:27:44 +02:00
Ran Benita
65e6e39b76 Merge pull request #7931 from bluetech/xunit-quadratic-2
fixtures: fix quadratic behavior in the number of autouse fixtures
2020-10-25 01:24:38 +03:00
Ran Benita
470ea504e2 fixtures: fix quadratic behavior in the number of autouse fixtures
It turns out all autouse fixtures are kept in a global list, and thinned
out for a particular node using a linear scan of the entire list each
time.

Change the list to a dict, and only take the nodes we need.
2020-10-25 00:49:23 +03:00
Ran Benita
d6becfa177 fixtures: change _getautousenames to an iterator
This reads better.
2020-10-25 00:49:06 +03:00
Ran Benita
aa0e2d654f fixtures: use a faster replacement for ischildnode
ischildnode can be quite hot in some cases involving many fixtures.
However it is always used in a way that the nodeid is constant and the
baseid is iterated. So we can save work by pre-computing the parents of
the nodeid and use a simple containment test.

The `_getautousenames` function has the same stuff open-coded, so change
it to use the new function as well.
2020-10-25 00:48:35 +03:00
Bruno Oliveira
f7d4f457d0 Merge pull request #7934 from symonk/small-adjustment-to-hookspec-docstrings
Replace term 'triple' with 'tuple' in hookspec docstrings
2020-10-24 08:01:05 -03:00
symonk
751575fa97 make some hookspec docstrings technically correct 2020-10-24 10:59:25 +01:00
Thomas Grainger
e14b724ff4 Update .github/ISSUE_TEMPLATE/2_feature_request.md 2020-10-23 22:35:56 +01:00
Bruno Oliveira
daa11ab9f1 Merge pull request #7930 from bluetech/pytester-doc-fixes
pytester: minor doc fixes
2020-10-23 17:21:22 -03:00
Ran Benita
0b14350f23 Merge pull request #7929 from bluetech/xunit-quadratic-1
python: fix quadratic behavior in collection of items using xunit fixtures
2020-10-23 23:10:25 +03:00
Ran Benita
50114d4731 python: fix quadratic behavior in collection of items using xunit fixtures
Since commit 0f918b1a9d pytest uses auto-generated autouse
pytest fixtures for the xunit fixtures
{setup,teardown}_{module,class,method,function}. All of these fixtures
were given the same name.

Unfortunately, pytest fixture lookup for a name works by grabbing all of
the fixtures globally declared with a name and filtering to only those
which match the specific node. So each xunit-using item iterates over a
list (of fixturedefs) of a size of all previous same-xunit-using items,
i.e. quadratic.

Fixing this properly to use a better data structure is likely to take
some effort, but we can avoid the immediate problem by just using
a different name for each item's autouse fixture, so it only matches
itself.

A benchmark is added to demonstrate the issue. It is still way too slow
after the fix and possibly still quadratic, but for a different reason
which is another matter.

Running --collect-only, before (snipped):

         202533232 function calls (201902604 primitive calls) in 86.379 seconds

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000   85.688   85.688 main.py:320(pytest_collection)
        1    0.000    0.000   85.688   85.688 main.py:567(perform_collect)
80557/556    0.021    0.000   85.050    0.153 {method 'extend' of 'list' objects}
85001/15001  0.166    0.000   85.045    0.006 main.py:785(genitems)
    10002    0.050    0.000   84.219    0.008 runner.py:455(collect_one_node)
    10002    0.049    0.000   83.763    0.008 runner.py:340(pytest_make_collect_report)
    10002    0.079    0.000   83.668    0.008 runner.py:298(from_call)
    10002    0.019    0.000   83.541    0.008 runner.py:341(<lambda>)
     5001    0.184    0.000   81.922    0.016 python.py:412(collect)
     5000    0.020    0.000   81.072    0.016 python.py:842(collect)
    30003    0.118    0.000   78.478    0.003 python.py:218(pytest_pycollect_makeitem)
    30000    0.190    0.000   77.957    0.003 python.py:450(_genfunctions)
    40001    0.081    0.000   76.664    0.002 nodes.py:183(from_parent)
    30000    0.087    0.000   76.629    0.003 python.py:1595(from_parent)
    40002    0.092    0.000   76.583    0.002 nodes.py:102(_create)
    30000    0.305    0.000   76.404    0.003 python.py:1533(__init__)
    15000    0.132    0.000   74.765    0.005 fixtures.py:1439(getfixtureinfo)
    15000    0.165    0.000   73.664    0.005 fixtures.py:1492(getfixtureclosure)
    15000    0.044    0.000   57.584    0.004 fixtures.py:1653(getfixturedefs)
    30000   18.840    0.001   57.540    0.002 fixtures.py:1668(_matchfactories)
 37507500   31.352    0.000   38.700    0.000 nodes.py:76(ischildnode)
    15000   10.464    0.001   15.806    0.001 fixtures.py:1479(_getautousenames)
112930587/112910019   7.333    0.000    7.339    0.000 {built-in method builtins.len}

After:

         51890333 function calls (51259706 primitive calls) in 27.306 seconds

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000   26.783   26.783 main.py:320(pytest_collection)
        1    0.000    0.000   26.783   26.783 main.py:567(perform_collect)
80557/556    0.020    0.000   26.108    0.047 {method 'extend' of 'list' objects}
85001/15001  0.151    0.000   26.103    0.002 main.py:785(genitems)
    10002    0.047    0.000   25.324    0.003 runner.py:455(collect_one_node)
    10002    0.045    0.000   24.888    0.002 runner.py:340(pytest_make_collect_report)
    10002    0.069    0.000   24.805    0.002 runner.py:298(from_call)
    10002    0.017    0.000   24.690    0.002 runner.py:341(<lambda>)
     5001    0.168    0.000   23.150    0.005 python.py:412(collect)
     5000    0.019    0.000   22.223    0.004 python.py:858(collect)
    30003    0.101    0.000   19.818    0.001 python.py:218(pytest_pycollect_makeitem)
    30000    0.161    0.000   19.368    0.001 python.py:450(_genfunctions)
    30000    0.302    0.000   18.236    0.001 python.py:1611(from_parent)
    40001    0.084    0.000   18.051    0.000 nodes.py:183(from_parent)
    40002    0.116    0.000   17.967    0.000 nodes.py:102(_create)
    30000    0.308    0.000   17.770    0.001 python.py:1549(__init__)
    15000    0.117    0.000   16.111    0.001 fixtures.py:1439(getfixtureinfo)
    15000    0.134    0.000   15.135    0.001 fixtures.py:1492(getfixtureclosure)
    15000    9.320    0.001   14.738    0.001 fixtures.py:1479(_getautousenames)
2020-10-23 22:36:23 +03:00
Ran Benita
1c0c56dfb9 pytester: minor doc fixes 2020-10-23 21:01:31 +03:00
Emiel van de Laar
0d9e27a363 doc: Remove unused imports in examples (#7924)
The "os" imports in the `tmp_path` and `tmpdir` fixture examples are
unused and thus have been removed to prevent confusion.
2020-10-23 17:31:17 +03:00
Ran Benita
0cdbf8b377 Merge pull request #7910 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-trio-0.7.0
build(deps): bump pytest-trio from 0.6.0 to 0.7.0 in /testing/plugins_integration
2020-10-23 16:52:54 +03:00
Ran Benita
e1848349a7 Merge pull request #7909 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-django-4.0.0
build(deps): bump pytest-django from 3.10.0 to 4.0.0 in /testing/plugins_integration
2020-10-23 16:52:29 +03:00
Thomas Grainger
03363473f7 expand feature request issue template 2020-10-23 14:03:24 +01:00
Matthias Gabriel
e8504e04f3 Fix small typo in reference.rst (#7922)
Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
2020-10-22 07:11:49 -03:00
Bruno Oliveira
824e9cf67a Merge pull request #7917 from bluetech/ci-timeout
ci: decrease job timeout from 6 hours to 30 minutes
2020-10-21 08:02:30 -03:00
Ran Benita
fe69d0d680 ci: decrease job timeout from 6 hours to 30 minutes
We don't have any jobs that should go beyond that, so let's be nicer to
the CI host and quicker to report the failure.
2020-10-21 10:17:25 +03:00
Ran Benita
a66b6b857a Merge pull request #7915 from bluetech/fix-lf-package
cacheprovider: fix some files in packages getting lost from --lf
2020-10-21 10:12:32 +03:00
Ran Benita
afaabdda8c cacheprovider: fix some files in packages getting lost from --lf
--lf has an optimization where it skips collecting Modules (python
files) which don't contain failing tests. The optimization works by
getting the paths of all cached failed tests and skipping the collection
of Modules whose path is not included in that list.

In pytest, Package nodes are Module nodes with the fspath being the file
`<package dir>/__init__.py`. Since it's a Module the logic above
triggered for it, and because it's an `__init__.py` file which is
unlikely to have any failing tests in it, it is skipped, which causes
its entire directory to be skipped, including any Modules inside it with
failing tests.

Fix by special-casing Packages to never filter. This means entire
Packages are never filtered, the Modules themselves are always checked.
It is reasonable to consider an optimization which does filter entire
packages bases on parent paths etc. but this wouldn't actually save any
real work so is really not worth it.
2020-10-19 19:02:43 +03:00
Bruno Oliveira
f453460ae7 Merge pull request #7912 from hugovk/rm-3.5 2020-10-19 10:27:49 -03:00
Hugo van Kemenade
c9e5042d6d Remove redundant Python 2.7 code 2020-10-19 10:47:35 +03:00
Hugo van Kemenade
a642650e17 Drop support for EOL Python 3.5 2020-10-19 10:02:36 +03:00
dependabot[bot]
f335144d1d build(deps): bump pytest-trio in /testing/plugins_integration
Bumps [pytest-trio](https://github.com/python-trio/pytest-trio) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/python-trio/pytest-trio/releases)
- [Commits](https://github.com/python-trio/pytest-trio/compare/v0.6.0...v0.7.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-19 03:05:42 +00:00
dependabot[bot]
23aac10391 build(deps): bump pytest-django in /testing/plugins_integration
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 3.10.0 to 4.0.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v3.10.0...v4.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-19 03:05:42 +00:00
Ran Benita
f61d4ed9d5 Merge pull request #7907 from bluetech/mypy-790
Update mypy 0.782 -> 0.790, iniconfig typing
2020-10-19 00:28:04 +03:00
Ran Benita
09e38b1697 runner: combine a sort+reverse to a sort(reverse=True)
Suggested by Zac-HD.
2020-10-19 00:02:01 +03:00
Ran Benita
1b23a111d2 Update mypy 0.782 -> 0.790 2020-10-17 19:25:45 +03:00
Ran Benita
e5e47c1097 Fix typing related to iniconfig
iniconfig now has typing stubs which reveal a couple issues.
2020-10-17 19:25:45 +03:00
Bruno Oliveira
0a258f534f Fix linting 2020-10-17 08:42:15 -03:00
Bruno Oliveira
f58d0a8c3d Merge pull request #7903 from ekrecker/patch-1
Fix typos
2020-10-17 08:41:10 -03:00
Nimesh Vashistha
991bc7bd50 Added note to writing_plugins.rst (#7896)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-10-17 08:26:30 -03:00
kwgchi
fc70fd23a2 Fix typos 2020-10-16 21:29:58 +09:00
Bruno Oliveira
b4c28dcaa2 Merge pull request #7893 from nicoddemus/testdir-docs-7892
List Testdir members in the docs
2020-10-14 10:34:49 -03:00
Prashant Sharma
5182c73fea Add example for registering multiple custom mark (#7886) 2020-10-14 15:17:50 +03:00
Bruno Oliveira
3cae145e41 List Testdir members in the docs
Also include docstrings pointing to the counterparts in Pytester.

Fix #7892
2020-10-13 12:01:11 -03:00
Bruno Oliveira
69419cb700 New pytester fixture (#7854) 2020-10-12 12:13:06 -03:00
Bruno Oliveira
cb578a918e Merge pull request #7883 from pytest-dev/dependabot/pip/testing/plugins_integration/django-3.1.2
build(deps): bump django from 3.1.1 to 3.1.2 in /testing/plugins_integration
2020-10-12 09:43:17 -03:00
dependabot[bot]
b53a8bb60f build(deps): bump django in /testing/plugins_integration
Bumps [django](https://github.com/django/django) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.1...3.1.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-12 12:39:31 +00:00
Bruno Oliveira
3434488af4 Merge pull request #7882 from pytest-dev/dependabot/pip/testing/plugins_integration/anyio-curiotrio--2.0.2
build(deps): bump anyio[curio,trio] from 2.0.0 to 2.0.2 in /testing/plugins_integration
2020-10-12 09:38:54 -03:00
Ronny Pfannschmidt
cdaa1b52be Merge pull request #7884 from bluetech/release-on-comment-fixes
release-on-comment: add "Closes <release issue number>" to release PR
2020-10-11 10:49:45 +02:00
Ran Benita
008863aeb9 release-on-comment: add "Closes <release issue number>" to release PR 2020-10-10 19:01:41 +03:00
dependabot[bot]
37cf4693cf build(deps): bump anyio[curio,trio] in /testing/plugins_integration
Bumps [anyio[curio,trio]](https://github.com/agronholm/anyio) from 2.0.0 to 2.0.2.
- [Release notes](https://github.com/agronholm/anyio/releases)
- [Changelog](https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst)
- [Commits](https://github.com/agronholm/anyio/compare/2.0.0...2.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-10 15:52:11 +00:00
Hugo van Kemenade
3059caf1ee Put smoke test deps in requirements.txt for Dependabot (#7806) 2020-10-10 18:51:35 +03:00
Ran Benita
55127b2142 Merge pull request #7879 from charlesaracil-ulti/ask_for_commit_after_in_contributing
ask for commit after changelog and authors file edit in contributing
2020-10-09 22:34:06 +03:00
Charles Aracil
2e322f183c ask for commit after changelog and authors file edit (#7878) 2020-10-09 16:10:54 +02:00
Zac Hatfield-Dodds
dbd082af96 Merge pull request #7874 from tanvimehta/master 2020-10-08 17:36:54 +11:00
Tanvi Mehta
d093931464 Added name to authors list 2020-10-07 22:57:52 -07:00
Tanvi Mehta
779b511bfe Fixed formatting 2020-10-07 22:25:27 -07:00
Tanvi Mehta
43b1eb3c9e Use instead of a in Issue #7868
Use `collections.Counter` instead of a `dict` in `terminal.py` Issue #7868
2020-10-07 21:51:28 -07:00
Kyle Altendorf
5acc55e838 Merge pull request #7872 from altendky/addini_takes_string
Document that Parser.addini() can take, and defaults to, 'string'
2020-10-07 19:12:54 -04:00
Kyle Altendorf
1630c37266 Added changelog/7872.doc.rst 2020-10-07 18:06:13 -04:00
Kyle Altendorf
76acb44330 Update tests to cover explicit None and "string" as addini() types 2020-10-07 17:56:54 -04:00
Kyle Altendorf
af3759a503 Parser.addini() can take and defaults to 'string' 2020-10-07 17:21:55 -04:00
Anthony Sottile
95917f8833 Merge pull request #7863 from asottile/py36_order_preserving_dict
py36+: remove _pytest.compat.order_preserving_dict
2020-10-06 08:35:38 -07:00
Manuel Mariñez
13ddec9a00 Add alias clarification to deprecation warning (#7829)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-10-06 11:48:34 -03:00
Anthony Sottile
b6b75383ce py36+: remove _pytest.compat.order_preserving_dict 2020-10-06 00:22:09 -07:00
Anthony Sottile
f54ec30a6d Merge pull request #7862 from asottile/comm2ann
py36+: com2ann
2020-10-06 00:20:57 -07:00
Anthony Sottile
33d119f71a py36+: com2ann 2020-10-05 18:33:17 -07:00
William Jamir Silva
703e89134c Update reference.rst informing the default junit_family (#7860)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-10-05 14:04:37 -03:00
Anthony Sottile
f81c6c00a9 Merge pull request #7852 from asottile/py36_pyupgrade
py36+: pyupgrade: py36+
2020-10-04 08:25:32 -07:00
Ran Benita
1c08f1dd0f Merge pull request #7844 from bluetech/typing-fixtures
fixtures: some type annotations, remove cyclic dependency
2020-10-04 17:41:33 +03:00
Bruno Oliveira
7581f0b3a1 Merge pull request #7853 from albertvillanova/doc-patch-1 2020-10-04 09:15:46 -03:00
Albert Villanova del Moral
8593b57666 Update link to numpy 2020-10-04 08:54:43 +02:00
Ran Benita
d0a3f1dcbc nodes: remove cyclic dependency on _pytest.fixtures
- Change the fixtures plugin to store its one piece of data on the node's
  Store instead of directly.

- Import FixtureLookupError lazily.
2020-10-04 09:44:45 +03:00
Ran Benita
bf09e7792f fixtures: some type annotations 2020-10-04 09:43:58 +03:00
Ran Benita
7f794b7aff Merge pull request #7851 from bluetech/cherry-pick-release
Merge pull request #7849 from pytest-dev/release-6.1.1
2020-10-03 22:57:41 +03:00
Anthony Sottile
66bd44c13a py36+: pyupgrade: py36+ 2020-10-03 12:46:54 -07:00
Anthony Sottile
b1bcb9fba8 Merge pull request #7850 from asottile/py36_pathlib_export
py36+: remove rexport of Path and PurePath
2020-10-03 12:43:25 -07:00
Ran Benita
fd74dd3dcb Merge pull request #7849 from pytest-dev/release-6.1.1
Prepare release 6.1.1

(cherry picked from commit 69d903260d)
2020-10-03 22:38:33 +03:00
Anthony Sottile
fb1d550aac py36+: remove rexport of Path and PurePath 2020-10-03 12:16:52 -07:00
Anthony Sottile
022ac9b9e8 Merge pull request #7846 from asottile/py36_black
py36+: update the target version of black to py36
2020-10-03 11:45:45 -07:00
Anthony Sottile
3b957c2244 Merge pull request #7845 from pytest-dev/py36_overload
py36+: remove _pytest.compat.overload
2020-10-03 11:45:29 -07:00
Hugo van Kemenade
133e8af4ee Merge pull request #7805 from hugovk/pytest-rerunfailures
Smoke test pytest-rerunfailures
2020-10-03 20:17:06 +03:00
Anthony Sottile
f295b0267d py36+: update the target version of black to py36 2020-10-03 08:17:22 -07:00
Anthony Sottile
7f0d2beb50 py36+: remove _pytest.compat.overload 2020-10-03 08:01:22 -07:00
Anthony Sottile
6ed07a1c25 Merge pull request #7840 from asottile/py36_typing_Type
py36+: from typing import Type: no longer need guard
2020-10-03 07:44:06 -07:00
Anthony Sottile
aa077ab188 Merge pull request #7841 from asottile/py36_todo
py36+: resolve py36 TODOs
2020-10-03 07:43:15 -07:00
Anthony Sottile
c2a197f351 Merge pull request #7839 from asottile/py36_compat_fspath
py36+: remove _pytest.compat.fspath
2020-10-03 07:42:54 -07:00
Ran Benita
5efddd32db Merge pull request #7826 from bluetech/doc-final
doc: patch Sphinx to detect our `@final` for marking classes as `final`
2020-10-03 13:29:17 +03:00
Ran Benita
7705e5e624 doc: patch Sphinx to detect our @final for marking classes as final
Thanks to Dominic Davis-Foster for code & assistance.
2020-10-03 13:13:14 +03:00
Ran Benita
a6a7ba57e0 Merge pull request #7817 from bluetech/fix-testpaths-bestrelpath2
terminal: fix crash in header reporting when absolute testpaths is used
2020-10-03 12:59:18 +03:00
Anthony Sottile
53b5f64b4b py36+: resolve py36 TODOs 2020-10-02 19:57:55 -07:00
Anthony Sottile
bfadd4060e py36+: from typing import Type: no longer need guard 2020-10-02 19:50:10 -07:00
Anthony Sottile
be43c7c67b py36+: remove _pytest.compat.fspath 2020-10-02 19:49:32 -07:00
Anthony Sottile
a23666d554 Merge pull request #7838 from asottile/py36_requires_ordered_markup
py36+: remove requires_ordered_markup
2020-10-02 19:48:01 -07:00
Anthony Sottile
ced0a52a87 Merge pull request #7837 from asottile/py36_union_pattern_match
py36+: remove workaround for Union[Pattern/Match] bug
2020-10-02 19:47:50 -07:00
Anthony Sottile
2c7b7d8f66 Merge pull request #7836 from asottile/py36_typing_X
py36+: replace typing.X with X
2020-10-02 19:47:42 -07:00
Anthony Sottile
ac189885f6 Merge pull request #7835 from asottile/py36_misc
py36+: miscellaneous (3, 6) cleanup
2020-10-02 19:47:35 -07:00
Anthony Sottile
6ba13ed528 Merge pull request #7834 from asottile/py36_TYPE_CHECKING
py36+: remove TYPE_CHECKING from _pytest.compat
2020-10-02 19:47:27 -07:00
Anthony Sottile
a6ef0f8f67 Merge pull request #7833 from asottile/py36_ModuleNotFoundError
py36+: remove _pytest.compat.MODULE_NOT_FOUND_ERROR
2020-10-02 19:47:18 -07:00
Anthony Sottile
7836c2c371 Merge pull request #7832 from asottile/py36_pathlib2
py36+: remove pathlib2 compatibility shim
2020-10-02 19:47:10 -07:00
Bruno Oliveira
6ee1eadd1c Fake setuptools-scm into using version 6.2.0a1
Due to pytest-rerunfailures latest version requiring 6.1.0, which is not
tagged on master.
2020-10-02 23:40:50 -03:00
Anthony Sottile
daba7ceb71 py36+: remove requires_ordered_markup 2020-10-02 15:06:59 -07:00
Anthony Sottile
e622cb7c41 py36+: remove workaround for Union[Pattern/Match] bug 2020-10-02 15:06:02 -07:00
Anthony Sottile
cf220b92a2 py36+: replace typing.X with X 2020-10-02 15:05:13 -07:00
Anthony Sottile
284fd45a08 py36+: miscellaneous (3, 6) cleanup 2020-10-02 15:04:16 -07:00
Anthony Sottile
a238d1f37d py36+: remove TYPE_CHECKING from _pytest.compat
automated with:

```bash
git grep -l 'from .* import TYPE_CHECKING' |
    xargs reorder-python-imports \
        --application-directories .:src \
        --remove-import 'from _pytest.compat import TYPE_CHECKING' \
        --add-import 'from typing import TYPE_CHECKING'
```
2020-10-02 15:03:24 -07:00
Anthony Sottile
1f57fb079d py36+: remove _pytest.compat.MODULE_NOT_FOUND_ERROR 2020-10-02 15:02:45 -07:00
Anthony Sottile
3c93eb0f04 py36+: remove pathlib2 compatibility shim 2020-10-02 14:59:07 -07:00
Anthony Sottile
325b988ca8 Merge pull request #7831 from asottile/py36_drop_in_CI
py36+: drop python3.5 in CI and setup.cfg
2020-10-02 14:55:02 -07:00
Anthony Sottile
179f4326df py36+: drop python3.5 in CI and setup.cfg 2020-10-02 14:00:11 -07:00
Max Voitko
cb0a13a523 Fix minor typos in doctest.rst (#7828) 2020-10-02 15:39:15 +02:00
Ran Benita
b250c9d615 Merge pull request #7813 from bluetech/findpaths-confusion
findpaths: fix regression causing incorrect rootdir to be determined
2020-09-30 13:21:18 +03:00
Ran Benita
3ecdad67b7 terminal: improve condition on whether to display testpaths in header
Make it match better the condition on whether testpaths is used (found
in config/__init__.py).
2020-09-29 15:23:47 +03:00
Ran Benita
61f80a783a terminal: fix crash in header reporting when absolute testpaths is used
Regressed in 6.1.0 in 62e249a1f9.
The `x` is an `str` but is expected to be a `pathlib.Path`. Not caught
by mypy because `config.getini()` returns `Any`.

Fix by just removing the `bestrelpath` call:

- testpaths are always relative to the rootdir, it thus would be very
  unusual to specify an absolute path there.

- The code was wrong even before the regression: `py.path.local`'s
  `bestrelpath` function expects a `py.path.local`, not an `str`. But it
  had some weird `try ... except AttributeError` fallback which just
  returns the argument, i.e. it was a no-op. So there is no behavior
  change.

- It seems reasonable to me to just print the full path if that's what
  the ini specifies.
2020-09-29 15:23:47 +03:00
Ran Benita
db08c7fbb0 pathlib: improve comments on commonpath and bestrelpath 2020-09-29 13:11:47 +03:00
Hugo van Kemenade
875f226d3b Smoke test pytest-rerunfailures 2020-09-29 12:47:46 +03:00
Ran Benita
cd67c2a8cf Merge pull request #7802 from bluetech/bump-attrs
Bump attrs requirement from >=17.4.0 to >=19.2.0
2020-09-28 19:30:58 +03:00
Ran Benita
4a9192f727 findpaths: fix regression causing incorrect rootdir to be determined
When switching from py.path.local to pathlib (70f3ad1c1f),
`local.parts(reverse=True)` was translated incorrectly, leading to the
wrong rootdir being determined in some non-trivial cases where parent
directories have config files as well.
2020-09-28 19:13:01 +03:00
Jakob van Santen
91fa11bed0 python_api: let approx() take nonnumeric values (#7710)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2020-09-28 12:17:23 -03:00
Ran Benita
f324b27d02 Merge pull request #7748 from nicoddemus/fix-plugin-order-docs
Improve docs about plugin discovery/loading at startup
2020-09-27 13:19:58 +03:00
Ran Benita
32bb8f3a63 Bump attrs requirement from >=17.4.0 to >=19.2.0
This allows us to remove the `ATTRS_EQ_FIELD` thing which is causing
some annoyance.
2020-09-27 13:17:59 +03:00
Anthony Sottile
28ba9ab737 Merge pull request #7801 from pytest-dev/fix-bot-typo
Fix typos in pytestbot
2020-09-26 15:53:14 -07:00
Hugo van Kemenade
14de6781d8 Fix typos in pytestbot 2020-09-26 23:46:58 +03:00
Bruno Oliveira
7324e90199 Update doc/en/writing_plugins.rst
Co-authored-by: Ran Benita <ran@unusedvar.com>
2020-09-19 16:14:28 -03:00
Bruno Oliveira
541b30a044 Improve docs about plugin discovery/loading at startup
Fix #7691
2020-09-12 14:11:43 -03:00
181 changed files with 10781 additions and 5912 deletions

View File

@@ -3,3 +3,23 @@ name: 🚀 Feature Request
about: Ideas for new features and improvements
---
<!--
Thanks for suggesting a feature!
Quick check-list while suggesting features:
-->
#### What's the problem this feature will solve?
<!-- What are you trying to do, that you are unable to achieve with pytest as it currently stands? -->
#### Describe the solution you'd like
<!-- A clear and concise description of what you want to happen. -->
<!-- Provide examples of real-world use cases that this would enable and how it solves the problem described above. -->
#### Alternative Solutions
<!-- Have you tried to workaround the problem using a pytest plugin or other tools? Or a different approach to solving this issue? Please elaborate here. -->
#### Additional context
<!-- Add any other context, links, etc. about the feature here. -->

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/testing/plugins_integration"
schedule:
interval: weekly
time: "03:00"
open-pull-requests-limit: 10
allow:
- dependency-type: direct
- dependency-type: indirect

View File

@@ -6,7 +6,8 @@ on:
- master
- "[0-9]+.[0-9]+.x"
tags:
- "*"
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
pull_request:
branches:
@@ -16,18 +17,17 @@ on:
jobs:
build:
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
name: [
"windows-py35",
"windows-py36",
"windows-py37",
"windows-py37-pluggy",
"windows-py38",
"ubuntu-py35",
"ubuntu-py36",
"ubuntu-py37",
"ubuntu-py37-pluggy",
@@ -45,11 +45,6 @@ jobs:
]
include:
- name: "windows-py35"
python: "3.5"
os: windows-latest
tox_env: "py35-xdist"
use_coverage: true
- name: "windows-py36"
python: "3.6"
os: windows-latest
@@ -68,10 +63,6 @@ jobs:
tox_env: "py38-unittestextras"
use_coverage: true
- name: "ubuntu-py35"
python: "3.5"
os: ubuntu-latest
tox_env: "py35-xdist"
- name: "ubuntu-py36"
python: "3.6"
os: ubuntu-latest
@@ -79,7 +70,7 @@ jobs:
- name: "ubuntu-py37"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-lsof-numpy-oldattrs-pexpect"
tox_env: "py37-lsof-numpy-pexpect"
use_coverage: true
- name: "ubuntu-py37-pluggy"
python: "3.7"
@@ -94,7 +85,7 @@ jobs:
os: ubuntu-latest
tox_env: "py38-xdist"
- name: "ubuntu-py39"
python: "3.9-dev"
python: "3.9"
os: ubuntu-latest
tox_env: "py39-xdist"
- name: "ubuntu-pypy3"
@@ -133,12 +124,6 @@ jobs:
fetch-depth: 0
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v2
if: matrix.python != '3.9-dev'
with:
python-version: ${{ matrix.python }}
- name: Set up Python ${{ matrix.python }} (deadsnakes)
uses: deadsnakes/action@v2.0.0
if: matrix.python == '3.9-dev'
with:
python-version: ${{ matrix.python }}
- name: Install dependencies
@@ -175,18 +160,22 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: set PY
run: echo "::set-env name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')"
- uses: actions/cache@v1
run: echo "name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV
- uses: actions/cache@v2
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
- run: pip install tox
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- run: tox -e linting
deploy:
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest'
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [build]

1
.gitignore vendored
View File

@@ -34,6 +34,7 @@ issue/
env/
.env/
.venv/
/pythonenv*/
3rdparty/
.tox
.cache

View File

@@ -5,12 +5,12 @@ repos:
- id: black
args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs
rev: v1.7.0
rev: v1.8.0
hooks:
- id: blacken-docs
additional_dependencies: [black==19.10b0]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -21,7 +21,7 @@ repos:
exclude: _pytest/(debugging|hookspec).py
language_version: python3
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.2
rev: 3.8.3
hooks:
- id: flake8
language_version: python3
@@ -29,27 +29,36 @@ repos:
- flake8-typing-imports==1.9.0
- flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.3.0
rev: v2.3.5
hooks:
- id: reorder-python-imports
args: ['--application-directories=.:src', --py3-plus]
args: ['--application-directories=.:src', --py36-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v2.4.4
rev: v2.7.2
hooks:
- id: pyupgrade
args: [--py3-plus]
args: [--py36-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.9.0
rev: v1.11.0
hooks:
- id: setup-cfg-fmt
# TODO: when upgrading setup-cfg-fmt this can be removed
args: [--max-py-version=3.9]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.6.0
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.780 # NOTE: keep this in sync with setup.cfg.
rev: v0.790
hooks:
- id: mypy
files: ^(src/|testing/)
args: []
additional_dependencies:
- iniconfig>=1.1.0
- py>=1.8.2
- attrs>=19.2.0
- packaging
- repo: local
hooks:
- id: rst

View File

@@ -1,60 +0,0 @@
language: python
dist: trusty
python: '3.5.1'
cache: false
env:
global:
- PYTEST_ADDOPTS=-vv
# setuptools-scm needs all tags in order to obtain a proper version
git:
depth: false
install:
- python -m pip install --upgrade --pre tox
jobs:
include:
# Coverage for Python 3.5.{0,1} specific code, mostly typing related.
- env: TOXENV=py35 PYTEST_COVERAGE=1 PYTEST_ADDOPTS="-k test_raises_cyclic_reference"
before_install:
# Work around https://github.com/jaraco/zipp/issues/40.
- python -m pip install -U 'setuptools>=34.4.0' virtualenv==16.7.9
before_script:
- |
# Do not (re-)upload coverage with cron runs.
if [[ "$TRAVIS_EVENT_TYPE" = cron ]]; then
PYTEST_COVERAGE=0
fi
- |
if [[ "$PYTEST_COVERAGE" = 1 ]]; then
export COVERAGE_FILE="$PWD/.coverage"
export COVERAGE_PROCESS_START="$PWD/.coveragerc"
export _PYTEST_TOX_COVERAGE_RUN="coverage run -m"
export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
fi
script: tox
after_success:
- |
if [[ "$PYTEST_COVERAGE" = 1 ]]; then
env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh -F Travis
fi
notifications:
irc:
channels:
- "chat.freenode.net#pytest"
on_success: change
on_failure: change
skip_join: true
email:
- pytest-commit@python.org
branches:
only:
- master
- /^\d+\.\d+\.x$/

16
AUTHORS
View File

@@ -21,6 +21,7 @@ Anders Hovmöller
Andras Mitzki
Andras Tim
Andrea Cimatoribus
Andreas Motl
Andreas Zeidler
Andrey Paramonov
Andrzej Klajnert
@@ -32,6 +33,7 @@ Anthony Sottile
Anton Lodder
Antony Lee
Arel Cordero
Ariel Pillemer
Armin Rigo
Aron Coyle
Aron Curzon
@@ -55,11 +57,13 @@ Charles Cloud
Charles Machalow
Charnjit SiNGH (CCSJ)
Chris Lamb
Chris NeJame
Christian Boelsen
Christian Fetzer
Christian Neumüller
Christian Theunert
Christian Tismer
Christine Mecklenborg
Christoph Buelter
Christopher Dignam
Christopher Gilling
@@ -87,6 +91,7 @@ Dhiren Serai
Diego Russo
Dmitry Dygalo
Dmitry Pribysh
Dominic Mortlock
Duncan Betts
Edison Gustavo Muenz
Edoardo Batini
@@ -107,6 +112,7 @@ Florian Bruhin
Florian Dahlitz
Floris Bruynooghe
Gabriel Reis
Garvit Shubham
Gene Wood
George Kussumoto
Georgy Dyuldin
@@ -129,6 +135,7 @@ Ilya Konstantinov
Ionuț Turturică
Iwan Briquemont
Jaap Broekhuizen
Jakob van Santen
Jakub Mitoraj
Jan Balster
Janne Vanhala
@@ -153,6 +160,7 @@ Justyna Janczyszyn
Kale Kundert
Kamran Ahmad
Karl O. Pinc
Karthikeyan Singaravelan
Katarzyna Jachim
Katarzyna Król
Katerina Koukiou
@@ -195,6 +203,7 @@ Matthias Hafner
Maxim Filipenko
Maximilian Cosmo Sitter
mbyt
Mickey Pashov
Michael Aquilina
Michael Birtwell
Michael Droettboom
@@ -227,11 +236,14 @@ Pauli Virtanen
Pavel Karateev
Paweł Adamczak
Pedro Algarvio
Petter Strandmark
Philipp Loose
Pieter Mulder
Piotr Banaszkiewicz
Piotr Helm
Prakhar Gurunani
Prashant Anand
Prashant Sharma
Pulkit Goyal
Punyashloka Biswal
Quentin Pradet
@@ -256,10 +268,12 @@ Ryan Wooden
Samuel Dion-Girardeau
Samuel Searles-Bryant
Samuele Pedroni
Sanket Duthade
Sankt Petersbug
Segev Finer
Serhii Mozghovyi
Seth Junot
Shubham Adep
Simon Gomizelj
Simon Kerr
Skylar Downes
@@ -274,6 +288,7 @@ Sven-Hendrik Haase
Sylvain Marié
Tadek Teleżyński
Takafumi Arakaki
Tanvi Mehta
Tarcisio Fischer
Tareq Alayan
Ted Xiao
@@ -310,3 +325,4 @@ Xuecong Liao
Yoav Caspi
Zac Hatfield-Dodds
Zoltán Máté
Zsolt Cserna

View File

@@ -299,12 +299,6 @@ Here is a simple overview, with pytest-specific bits:
$ pytest testing/test_config.py
#. Commit and push once your tests pass and you are happy with your change(s)::
$ git commit -a -m "<commit message>"
$ git push -u
#. Create a new changelog entry in ``changelog``. The file should be named ``<issueid>.<type>.rst``,
where *issueid* is the number of the issue related to the change and *type* is one of
``feature``, ``improvement``, ``bugfix``, ``doc``, ``deprecation``, ``breaking``, ``vendor``
@@ -313,6 +307,11 @@ Here is a simple overview, with pytest-specific bits:
#. Add yourself to ``AUTHORS`` file if not there yet, in alphabetical order.
#. Commit and push once your tests pass and you are happy with your change(s)::
$ git commit -a -m "<commit message>"
$ git push -u
#. Finally, submit a pull request through the GitHub website using this data::
head-fork: YOUR_GITHUB_USERNAME/pytest

View File

@@ -89,7 +89,7 @@ Features
- Can run `unittest <https://docs.pytest.org/en/stable/unittest.html>`_ (or trial),
`nose <https://docs.pytest.org/en/stable/nose.html>`_ test suites out of the box
- Python 3.5+ and PyPy3
- Python 3.6+ and PyPy3
- Rich plugin architecture, with over 850+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community

View File

@@ -122,6 +122,14 @@ Both automatic and manual processes described above follow the same steps from t
#. Open a PR for ``cherry-pick-release`` and merge it once CI passes. No need to wait for approvals if there were no conflicts on the previous step.
#. For major and minor releases, tag the release cherry-pick merge commit in master with
a dev tag for the next feature release::
git checkout master
git pull
git tag MAJOR.{MINOR+1}.0.dev0
git push git@github.com:pytest-dev/pytest.git MAJOR.{MINOR+1}.0.dev0
#. Send an email announcement with the contents from::
doc/en/announce/release-<VERSION>.rst

13
bench/unit_test.py Normal file
View File

@@ -0,0 +1,13 @@
from unittest import TestCase # noqa: F401
for i in range(15000):
exec(
f"""
class Test{i}(TestCase):
@classmethod
def setUpClass(cls): pass
def test_1(self): pass
def test_2(self): pass
def test_3(self): pass
"""
)

11
bench/xunit.py Normal file
View File

@@ -0,0 +1,11 @@
for i in range(5000):
exec(
f"""
class Test{i}:
@classmethod
def setup_class(cls): pass
def test_1(self): pass
def test_2(self): pass
def test_3(self): pass
"""
)

View File

@@ -6,6 +6,11 @@ Release announcements
:maxdepth: 2
release-6.2.2
release-6.2.1
release-6.2.0
release-6.1.2
release-6.1.1
release-6.1.0
release-6.0.2
release-6.0.1

View File

@@ -0,0 +1,18 @@
pytest-6.1.1
=======================================
pytest 6.1.1 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/stable/changelog.html.
Thanks to all of the contributors to this release:
* Ran Benita
Happy testing,
The pytest Development Team

View File

@@ -0,0 +1,22 @@
pytest-6.1.2
=======================================
pytest 6.1.2 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/stable/changelog.html.
Thanks to all of the contributors to this release:
* Bruno Oliveira
* Manuel Mariñez
* Ran Benita
* Vasilis Gerakaris
* William Jamir Silva
Happy testing,
The pytest Development Team

View File

@@ -0,0 +1,76 @@
pytest-6.2.0
=======================================
The pytest team is proud to announce the 6.2.0 release!
This release contains new features, improvements, bug fixes, and breaking changes, so users
are encouraged to take a look at the CHANGELOG carefully:
https://docs.pytest.org/en/stable/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/stable/
As usual, you can upgrade from PyPI via:
pip install -U pytest
Thanks to all of the contributors to this release:
* Adam Johnson
* Albert Villanova del Moral
* Anthony Sottile
* Anton
* Ariel Pillemer
* Bruno Oliveira
* Charles Aracil
* Christine M
* Christine Mecklenborg
* Cserna Zsolt
* Dominic Mortlock
* Emiel van de Laar
* Florian Bruhin
* Garvit Shubham
* Gustavo Camargo
* Hugo Martins
* Hugo van Kemenade
* Jakob van Santen
* Josias Aurel
* Jürgen Gmach
* Karthikeyan Singaravelan
* Katarzyna
* Kyle Altendorf
* Manuel Mariñez
* Matthew Hughes
* Matthias Gabriel
* Max Voitko
* Maximilian Cosmo Sitter
* Mikhail Fesenko
* Nimesh Vashistha
* Pedro Algarvio
* Petter Strandmark
* Prakhar Gurunani
* Prashant Sharma
* Ran Benita
* Ronny Pfannschmidt
* Sanket Duthade
* Shubham Adep
* Simon K
* Tanvi Mehta
* Thomas Grainger
* Tim Hoffmann
* Vasilis Gerakaris
* William Jamir Silva
* Zac Hatfield-Dodds
* crricks
* dependabot[bot]
* duthades
* frankgerhardt
* kwgchi
* mickeypash
* symonk
Happy testing,
The pytest Development Team

View File

@@ -0,0 +1,20 @@
pytest-6.2.1
=======================================
pytest 6.2.1 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/stable/changelog.html.
Thanks to all of the contributors to this release:
* Bruno Oliveira
* Jakob van Santen
* Ran Benita
Happy testing,
The pytest Development Team

View File

@@ -0,0 +1,21 @@
pytest-6.2.2
=======================================
pytest 6.2.2 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/stable/changelog.html.
Thanks to all of the contributors to this release:
* Adam Johnson
* Bruno Oliveira
* Chris NeJame
* Ran Benita
Happy testing,
The pytest Development Team

View File

@@ -74,7 +74,7 @@ Assertions about expected exceptions
------------------------------------------
In order to write assertions about raised exceptions, you can use
``pytest.raises`` as a context manager like this:
:func:`pytest.raises` as a context manager like this:
.. code-block:: python
@@ -123,7 +123,7 @@ The regexp parameter of the ``match`` method is matched with the ``re.search``
function, so in the above example ``match='123'`` would have worked as
well.
There's an alternate form of the ``pytest.raises`` function where you pass
There's an alternate form of the :func:`pytest.raises` function where you pass
a function that will be executed with the given ``*args`` and ``**kwargs`` and
assert that the given exception is raised:
@@ -144,8 +144,8 @@ specific way than just having any exception raised:
def test_f():
f()
Using ``pytest.raises`` is likely to be better for cases where you are testing
exceptions your own code is deliberately raising, whereas using
Using :func:`pytest.raises` is likely to be better for cases where you are
testing exceptions your own code is deliberately raising, whereas using
``@pytest.mark.xfail`` with a check function is probably better for something
like documenting unfixed bugs (where the test describes what "should" happen)
or bugs in dependencies.

View File

@@ -10,7 +10,7 @@ we keep learning about new and better structures to express different details ab
While we implement those modifications we try to ensure an easy transition and don't want to impose unnecessary churn on our users and community/plugin authors.
As of now, pytest considers multipe types of backward compatibility transitions:
As of now, pytest considers multiple types of backward compatibility transitions:
a) trivial: APIs which trivially translate to the new mechanism,
and do not cause problematic changes.
@@ -25,7 +25,7 @@ b) transitional: the old and new API don't conflict
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn them into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
c) true breakage: should only to be considered when normal transition is unreasonably unsustainable and would offset important development/features by years.
c) true breakage: should only be considered when normal transition is unreasonably unsustainable and would offset important development/features by years.
In addition, they should be limited to APIs where the number of actual users is very small (for example only impacting some plugins), and can be coordinated with the community in advance.
Examples for such upcoming changes:
@@ -42,7 +42,7 @@ c) true breakage: should only to be considered when normal transition is unreaso
After there's no hard *-1* on the issue it should be followed up by an initial proof-of-concept Pull Request.
This POC serves as both a coordination point to assess impact and potential inspriation to come up with a transitional solution after all.
This POC serves as both a coordination point to assess impact and potential inspiration to come up with a transitional solution after all.
After a reasonable amount of time the PR can be merged to base a new major release.

View File

@@ -158,6 +158,11 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
function invocation, created as a sub directory of the base temporary
directory.
By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
The returned object is a `py.path.local`_ path object.
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
@@ -167,12 +172,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
function invocation, created as a sub directory of the base temporary
directory.
By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
The returned object is a :class:`pathlib.Path` object.
.. note::
In python < 3.6 this is a pathlib2.Path.
no tests ran in 0.12s

View File

@@ -28,6 +28,268 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 6.2.2 (2021-01-25)
=========================
Bug Fixes
---------
- `#8152 <https://github.com/pytest-dev/pytest/issues/8152>`_: Fixed "(<Skipped instance>)" being shown as a skip reason in the verbose test summary line when the reason is empty.
- `#8249 <https://github.com/pytest-dev/pytest/issues/8249>`_: Fix the ``faulthandler`` plugin for occasions when running with ``twisted.logger`` and using ``pytest --capture=no``.
pytest 6.2.1 (2020-12-15)
=========================
Bug Fixes
---------
- `#7678 <https://github.com/pytest-dev/pytest/issues/7678>`_: Fixed bug where ``ImportPathMismatchError`` would be raised for files compiled in
the host and loaded later from an UNC mounted path (Windows).
- `#8132 <https://github.com/pytest-dev/pytest/issues/8132>`_: Fixed regression in ``approx``: in 6.2.0 ``approx`` no longer raises
``TypeError`` when dealing with non-numeric types, falling back to normal comparison.
Before 6.2.0, array types like tf.DeviceArray fell through to the scalar case,
and happened to compare correctly to a scalar if they had only one element.
After 6.2.0, these types began failing, because they inherited neither from
standard Python number hierarchy nor from ``numpy.ndarray``.
``approx`` now converts arguments to ``numpy.ndarray`` if they expose the array
protocol and are not scalars. This treats array-like objects like numpy arrays,
regardless of size.
pytest 6.2.0 (2020-12-12)
=========================
Breaking Changes
----------------
- `#7808 <https://github.com/pytest-dev/pytest/issues/7808>`_: pytest now supports python3.6+ only.
Deprecations
------------
- `#7469 <https://github.com/pytest-dev/pytest/issues/7469>`_: Directly constructing/calling the following classes/functions is now deprecated:
- ``_pytest.cacheprovider.Cache``
- ``_pytest.cacheprovider.Cache.for_config()``
- ``_pytest.cacheprovider.Cache.clear_cache()``
- ``_pytest.cacheprovider.Cache.cache_dir_from_config()``
- ``_pytest.capture.CaptureFixture``
- ``_pytest.fixtures.FixtureRequest``
- ``_pytest.fixtures.SubRequest``
- ``_pytest.logging.LogCaptureFixture``
- ``_pytest.pytester.Pytester``
- ``_pytest.pytester.Testdir``
- ``_pytest.recwarn.WarningsRecorder``
- ``_pytest.recwarn.WarningsChecker``
- ``_pytest.tmpdir.TempPathFactory``
- ``_pytest.tmpdir.TempdirFactory``
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0.
- `#7530 <https://github.com/pytest-dev/pytest/issues/7530>`_: The ``--strict`` command-line option has been deprecated, use ``--strict-markers`` instead.
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing flag for all strictness
related options (``--strict-markers`` and ``--strict-config`` at the moment, more might be introduced in the future).
- `#7988 <https://github.com/pytest-dev/pytest/issues/7988>`_: The ``@pytest.yield_fixture`` decorator/function is now deprecated. Use :func:`pytest.fixture` instead.
``yield_fixture`` has been an alias for ``fixture`` for a very long time, so can be search/replaced safely.
Features
--------
- `#5299 <https://github.com/pytest-dev/pytest/issues/5299>`_: pytest now warns about unraisable exceptions and unhandled thread exceptions that occur in tests on Python>=3.8.
See :ref:`unraisable` for more information.
- `#7425 <https://github.com/pytest-dev/pytest/issues/7425>`_: New :fixture:`pytester` fixture, which is identical to :fixture:`testdir` but its methods return :class:`pathlib.Path` when appropriate instead of ``py.path.local``.
This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future.
Internally, the old :class:`Testdir <_pytest.pytester.Testdir>` is now a thin wrapper around :class:`Pytester <_pytest.pytester.Pytester>`, preserving the old interface.
- `#7695 <https://github.com/pytest-dev/pytest/issues/7695>`_: A new hook was added, `pytest_markeval_namespace` which should return a dictionary.
This dictionary will be used to augment the "global" variables available to evaluate skipif/xfail/xpass markers.
Pseudo example
``conftest.py``:
.. code-block:: python
def pytest_markeval_namespace():
return {"color": "red"}
``test_func.py``:
.. code-block:: python
@pytest.mark.skipif("color == 'blue'", reason="Color is not red")
def test_func():
assert False
- `#8006 <https://github.com/pytest-dev/pytest/issues/8006>`_: It is now possible to construct a :class:`~pytest.MonkeyPatch` object directly as ``pytest.MonkeyPatch()``,
in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it
from the private `_pytest.monkeypatch.MonkeyPatch` namespace.
Additionally, :meth:`MonkeyPatch.context <pytest.MonkeyPatch.context>` is now a classmethod,
and can be used as ``with MonkeyPatch.context() as mp: ...``. This is the recommended way to use
``MonkeyPatch`` directly, since unlike the ``monkeypatch`` fixture, an instance created directly
is not ``undo()``-ed automatically.
Improvements
------------
- `#1265 <https://github.com/pytest-dev/pytest/issues/1265>`_: Added an ``__str__`` implementation to the :class:`~pytest.pytester.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method.
- `#2044 <https://github.com/pytest-dev/pytest/issues/2044>`_: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS".
- `#7469 <https://github.com/pytest-dev/pytest/issues/7469>`_ The types of builtin pytest fixtures are now exported so they may be used in type annotations of test functions.
The newly-exported types are:
- ``pytest.FixtureRequest`` for the :fixture:`request` fixture.
- ``pytest.Cache`` for the :fixture:`cache` fixture.
- ``pytest.CaptureFixture[str]`` for the :fixture:`capfd` and :fixture:`capsys` fixtures.
- ``pytest.CaptureFixture[bytes]`` for the :fixture:`capfdbinary` and :fixture:`capsysbinary` fixtures.
- ``pytest.LogCaptureFixture`` for the :fixture:`caplog` fixture.
- ``pytest.Pytester`` for the :fixture:`pytester` fixture.
- ``pytest.Testdir`` for the :fixture:`testdir` fixture.
- ``pytest.TempdirFactory`` for the :fixture:`tmpdir_factory` fixture.
- ``pytest.TempPathFactory`` for the :fixture:`tmp_path_factory` fixture.
- ``pytest.MonkeyPatch`` for the :fixture:`monkeypatch` fixture.
- ``pytest.WarningsRecorder`` for the :fixture:`recwarn` fixture.
Constructing them is not supported (except for `MonkeyPatch`); they are only meant for use in type annotations.
Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0.
Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.
- `#7527 <https://github.com/pytest-dev/pytest/issues/7527>`_: When a comparison between :func:`namedtuple <collections.namedtuple>` instances of the same type fails, pytest now shows the differing field names (possibly nested) instead of their indexes.
- `#7615 <https://github.com/pytest-dev/pytest/issues/7615>`_: :meth:`Node.warn <_pytest.nodes.Node.warn>` now permits any subclass of :class:`Warning`, not just :class:`PytestWarning <pytest.PytestWarning>`.
- `#7701 <https://github.com/pytest-dev/pytest/issues/7701>`_: Improved reporting when using ``--collected-only``. It will now show the number of collected tests in the summary stats.
- `#7710 <https://github.com/pytest-dev/pytest/issues/7710>`_: Use strict equality comparison for non-numeric types in :func:`pytest.approx` instead of
raising :class:`TypeError`.
This was the undocumented behavior before 3.7, but is now officially a supported feature.
- `#7938 <https://github.com/pytest-dev/pytest/issues/7938>`_: New ``--sw-skip`` argument which is a shorthand for ``--stepwise-skip``.
- `#8023 <https://github.com/pytest-dev/pytest/issues/8023>`_: Added ``'node_modules'`` to default value for :confval:`norecursedirs`.
- `#8032 <https://github.com/pytest-dev/pytest/issues/8032>`_: :meth:`doClassCleanups <unittest.TestCase.doClassCleanups>` (introduced in :mod:`unittest` in Python and 3.8) is now called appropriately.
Bug Fixes
---------
- `#4824 <https://github.com/pytest-dev/pytest/issues/4824>`_: Fixed quadratic behavior and improved performance of collection of items using autouse fixtures and xunit fixtures.
- `#7758 <https://github.com/pytest-dev/pytest/issues/7758>`_: Fixed an issue where some files in packages are getting lost from ``--lf`` even though they contain tests that failed. Regressed in pytest 5.4.0.
- `#7911 <https://github.com/pytest-dev/pytest/issues/7911>`_: Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites.
- `#7913 <https://github.com/pytest-dev/pytest/issues/7913>`_: Fixed a crash or hang in :meth:`pytester.spawn <_pytest.pytester.Pytester.spawn>` when the :mod:`readline` module is involved.
- `#7951 <https://github.com/pytest-dev/pytest/issues/7951>`_: Fixed handling of recursive symlinks when collecting tests.
- `#7981 <https://github.com/pytest-dev/pytest/issues/7981>`_: Fixed symlinked directories not being followed during collection. Regressed in pytest 6.1.0.
- `#8016 <https://github.com/pytest-dev/pytest/issues/8016>`_: Fixed only one doctest being collected when using ``pytest --doctest-modules path/to/an/__init__.py``.
Improved Documentation
----------------------
- `#7429 <https://github.com/pytest-dev/pytest/issues/7429>`_: Add more information and use cases about skipping doctests.
- `#7780 <https://github.com/pytest-dev/pytest/issues/7780>`_: Classes which should not be inherited from are now marked ``final class`` in the API reference.
- `#7872 <https://github.com/pytest-dev/pytest/issues/7872>`_: ``_pytest.config.argparsing.Parser.addini()`` accepts explicit ``None`` and ``"string"``.
- `#7878 <https://github.com/pytest-dev/pytest/issues/7878>`_: In pull request section, ask to commit after editing changelog and authors file.
Trivial/Internal Changes
------------------------
- `#7802 <https://github.com/pytest-dev/pytest/issues/7802>`_: The ``attrs`` dependency requirement is now >=19.2.0 instead of >=17.4.0.
- `#8014 <https://github.com/pytest-dev/pytest/issues/8014>`_: `.pyc` files created by pytest's assertion rewriting now conform to the newer PEP-552 format on Python>=3.7.
(These files are internal and only interpreted by pytest itself.)
pytest 6.1.2 (2020-10-28)
=========================
Bug Fixes
---------
- `#7758 <https://github.com/pytest-dev/pytest/issues/7758>`_: Fixed an issue where some files in packages are getting lost from ``--lf`` even though they contain tests that failed. Regressed in pytest 5.4.0.
- `#7911 <https://github.com/pytest-dev/pytest/issues/7911>`_: Directories created by `tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites.
Improved Documentation
----------------------
- `#7815 <https://github.com/pytest-dev/pytest/issues/7815>`_: Improve deprecation warning message for ``pytest._fillfuncargs()``.
pytest 6.1.1 (2020-10-03)
=========================
Bug Fixes
---------
- `#7807 <https://github.com/pytest-dev/pytest/issues/7807>`_: Fixed regression in pytest 6.1.0 causing incorrect rootdir to be determined in some non-trivial cases where parent directories have config files as well.
- `#7814 <https://github.com/pytest-dev/pytest/issues/7814>`_: Fixed crash in header reporting when :confval:`testpaths` is used and contains absolute paths (regression in 6.1.0).
pytest 6.1.0 (2020-09-26)
=========================
@@ -1842,6 +2104,44 @@ Improved Documentation
- `#5416 <https://github.com/pytest-dev/pytest/issues/5416>`_: Fix PytestUnknownMarkWarning in run/skip example.
pytest 4.6.11 (2020-06-04)
==========================
Bug Fixes
---------
- `#6334 <https://github.com/pytest-dev/pytest/issues/6334>`_: Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``).
The upper case variants were never documented and the preferred form should be the lower case.
- `#7310 <https://github.com/pytest-dev/pytest/issues/7310>`_: Fix ``UnboundLocalError: local variable 'letter' referenced before
assignment`` in ``_pytest.terminal.pytest_report_teststatus()``
when plugins return report objects in an unconventional state.
This was making ``pytest_report_teststatus()`` skip
entering if-block branches that declare the ``letter`` variable.
The fix was to set the initial value of the ``letter`` before
the if-block cascade so that it always has a value.
pytest 4.6.10 (2020-05-08)
==========================
Features
--------
- `#6870 <https://github.com/pytest-dev/pytest/issues/6870>`_: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``.
Remark: while this is technically a new feature and according to our `policy <https://docs.pytest.org/en/latest/py27-py34-deprecation.html#what-goes-into-4-6-x-releases>`_ it should not have been backported, we have opened an exception in this particular case because it fixes a serious interaction with ``pytest-xdist``, so it can also be considered a bugfix.
Trivial/Internal Changes
------------------------
- `#6404 <https://github.com/pytest-dev/pytest/issues/6404>`_: Remove usage of ``parser`` module, deprecated in Python 3.9.
pytest 4.6.9 (2020-01-04)
=========================

View File

@@ -15,11 +15,13 @@
#
# The full version, including alpha/beta/rc tags.
# The short X.Y version.
import ast
import os
import sys
from typing import List
from typing import TYPE_CHECKING
from _pytest import __version__ as version
from _pytest.compat import TYPE_CHECKING
if TYPE_CHECKING:
import sphinx.application
@@ -398,3 +400,22 @@ def setup(app: "sphinx.application.Sphinx") -> None:
)
configure_logging(app)
# Make Sphinx mark classes with "final" when decorated with @final.
# We need this because we import final from pytest._compat, not from
# typing (for Python < 3.8 compat), so Sphinx doesn't detect it.
# To keep things simple we accept any `@final` decorator.
# Ref: https://github.com/pytest-dev/pytest/pull/7780
import sphinx.pycode.ast
import sphinx.pycode.parser
original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final
def patched_is_final(self, decorators: List[ast.expr]) -> bool:
if original_is_final(self, decorators):
return True
return any(
sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators
)
sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final

View File

@@ -18,6 +18,28 @@ Deprecated Features
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
:class:`PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
The ``--strict`` command-line option
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.2
The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
better conveys what the option does.
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
flag for all strictness related options (``--strict-markers`` and ``--strict-config``
at the moment, more might be introduced in the future).
The ``yield_fixture`` function/decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.2
``pytest.yield_fixture`` is a deprecated alias for :func:`pytest.fixture`.
It has been so for a very long time, so can be search/replaced safely.
The ``pytest_warning_captured`` hook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -2,7 +2,7 @@
Doctest integration for modules and test files
=========================================================
By default all files matching the ``test*.txt`` pattern will
By default, all files matching the ``test*.txt`` pattern will
be run through the python standard ``doctest`` module. You
can change the pattern by issuing:
@@ -77,15 +77,6 @@ putting them into a pytest.ini file like this:
[pytest]
addopts = --doctest-modules
.. note::
The builtin pytest doctest supports only ``doctest`` blocks, but if you are looking
for more advanced checking over *all* your documentation,
including doctests, ``.. codeblock:: python`` Sphinx directive support,
and any other examples your documentation may include, you may wish to
consider `Sybil <https://sybil.readthedocs.io/en/latest/index.html>`__.
It provides pytest integration out of the box.
Encoding
--------
@@ -113,7 +104,7 @@ lengthy exception stack traces you can just write:
.. code-block:: ini
[pytest]
doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
Alternatively, options can be enabled by an inline comment in the doc test
itself:
@@ -206,7 +197,7 @@ It is possible to use fixtures using the ``getfixture`` helper:
>>> ...
>>>
Note that the fixture needs to be defined in a place visible by pytest, for example a `conftest.py`
Note that the fixture needs to be defined in a place visible by pytest, for example, a `conftest.py`
file or plugin; normal python files containing docstrings are not normally scanned for fixtures
unless explicitly configured by :confval:`python_files`.
@@ -253,12 +244,32 @@ Note that like the normal ``conftest.py``, the fixtures are discovered in the di
Meaning that if you put your doctest with your source code, the relevant conftest.py needs to be in the same directory tree.
Fixtures will not be discovered in a sibling directory tree!
Skipping tests dynamically
^^^^^^^^^^^^^^^^^^^^^^^^^^
Skipping tests
^^^^^^^^^^^^^^
.. versionadded:: 4.4
For the same reasons one might want to skip normal tests, it is also possible to skip
tests inside doctests.
To skip a single check inside a doctest you can use the standard
`doctest.SKIP <https://docs.python.org/3/library/doctest.html#doctest.SKIP>`__ directive:
.. code-block:: python
def test_random(y):
"""
>>> random.random() # doctest: +SKIP
0.156231223
>>> 1 + 1
2
"""
This will skip the first check, but not the second.
pytest also allows using the standard pytest functions :func:`pytest.skip` and
:func:`pytest.xfail` inside doctests, which might be useful because you can
then skip/xfail tests based on external conditions:
You can use ``pytest.skip`` to dynamically skip doctests. For example:
.. code-block:: text
@@ -266,3 +277,35 @@ You can use ``pytest.skip`` to dynamically skip doctests. For example:
>>> if sys.platform.startswith('win'):
... pytest.skip('this doctest does not work on Windows')
...
>>> import fcntl
>>> ...
However using those functions is discouraged because it reduces the readability of the
docstring.
.. note::
:func:`pytest.skip` and :func:`pytest.xfail` behave differently depending
if the doctests are in a Python file (in docstrings) or a text file containing
doctests intermingled with text:
* Python modules (docstrings): the functions only act in that specific docstring,
letting the other docstrings in the same module execute as normal.
* Text files: the functions will skip/xfail the checks for the rest of the entire
file.
Alternatives
------------
While the built-in pytest support provides a good set of functionalities for using
doctests, if you use them extensively you might be interested in those external packages
which add many more features, and include pytest integration:
* `pytest-doctestplus <https://github.com/astropy/pytest-doctestplus>`__: provides
advanced doctest support and enables the testing of reStructuredText (".rst") files.
* `Sybil <https://sybil.readthedocs.io>`__: provides a way to test examples in
your documentation by parsing them from the documentation source and evaluating
the parsed examples as part of your normal test run.

View File

@@ -176,7 +176,7 @@ class TestRaises:
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
items = [1, 2, 3]
print("items is {!r}".format(items))
print(f"items is {items!r}")
a, b = items.pop()
def test_some_error(self):

View File

@@ -11,4 +11,4 @@ def pytest_runtest_setup(item):
return
mod = item.getparent(pytest.Module).obj
if hasattr(mod, "hello"):
print("mod.hello {!r}".format(mod.hello))
print(f"mod.hello {mod.hello!r}")

View File

@@ -0,0 +1,132 @@
<svg xmlns="http://www.w3.org/2000/svg" width="572" height="542">
<style>
text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
dominant-baseline: middle;
text-anchor: middle;
fill: #062886;
font-size: medium;
}
ellipse.fixture, rect.test {
fill: #eeffcc;
stroke: #007020;
stroke-width: 2;
}
text.fixture {
color: #06287e;
}
circle.class, circle.module, circle.package {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
text.class, text.module, text.package {
fill: #0e84b5;
}
line, path {
stroke: black;
stroke-width: 2;
fill: none;
}
</style>
<!-- main scope -->
<circle class="package" r="270" cx="286" cy="271" />
<!-- scope name -->
<defs>
<path d="M 26,271 A 260 260 0 0 1 546 271" id="testp"/>
</defs>
<text class="package">
<textPath href="#testp" startOffset="50%">tests</textPath>
</text>
<!-- subpackage -->
<circle class="package" r="140" cx="186" cy="271" />
<!-- scope name -->
<defs>
<path d="M 56,271 A 130 130 0 0 1 316 271" id="subpackage"/>
</defs>
<text class="package">
<textPath href="#subpackage" startOffset="50%">subpackage</textPath>
</text>
<!-- test_subpackage.py -->
<circle class="module" r="90" cx="186" cy="311" />
<!-- scope name -->
<defs>
<path d="M 106,311 A 80 80 0 0 1 266 311" id="testSubpackage"/>
</defs>
<text class="module">
<textPath href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath>
</text>
<!-- innermost -->
<line x1="186" x2="186" y1="271" y2="351"/>
<!-- mid -->
<path d="M 186 351 L 136 351 L 106 331 L 106 196" />
<!-- order -->
<path d="M 186 351 L 256 351 L 316 291 L 316 136" />
<!-- top -->
<path d="M 186 351 L 186 391 L 231 436 L 331 436" />
<ellipse class="fixture" rx="50" ry="25" cx="186" cy="271" />
<text x="186" y="271">innermost</text>
<rect class="test" width="110" height="50" x="131" y="326" />
<text x="186" y="351">test_order</text>
<ellipse class="fixture" rx="50" ry="25" cx="126" cy="196" />
<text x="126" y="196">mid</text>
<!-- scope order number -->
<mask id="testSubpackageOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="90" cx="186" cy="311" />
</mask>
<circle class="module" r="15" cx="96" cy="311" mask="url(#testSubpackageOrderMask)"/>
<text class="module" x="96" y="311">1</text>
<!-- scope order number -->
<mask id="subpackageOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="140" cx="186" cy="271" />
</mask>
<circle class="module" r="15" cx="46" cy="271" mask="url(#subpackageOrderMask)"/>
<text class="module" x="46" y="271">2</text>
<!-- scope order number -->
<mask id="testsOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="270" cx="286" cy="271" />
</mask>
<circle class="module" r="15" cx="16" cy="271" mask="url(#testsOrderMask)"/>
<text class="module" x="16" y="271">3</text>
<!-- test_top.py -->
<circle class="module" r="85" cx="441" cy="271" />
<!-- scope name -->
<defs>
<path d="M 366,271 A 75 75 0 0 1 516 271" id="testTop"/>
</defs>
<text class="module">
<textPath href="#testTop" startOffset="50%">test_top.py</textPath>
</text>
<!-- innermost -->
<line x1="441" x2="441" y1="306" y2="236"/>
<!-- order -->
<path d="M 441 306 L 376 306 L 346 276 L 346 136" />
<!-- top -->
<path d="M 441 306 L 441 411 L 411 436 L 331 436" />
<ellipse class="fixture" rx="50" ry="25" cx="441" cy="236" />
<text x="441" y="236">innermost</text>
<rect class="test" width="110" height="50" x="386" y="281" />
<text x="441" y="306">test_order</text>
<!-- scope order number -->
<mask id="testTopOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="85" cx="441" cy="271" />
</mask>
<circle class="module" r="15" cx="526" cy="271" mask="url(#testTopOrderMask)"/>
<text class="module" x="526" y="271">1</text>
<!-- scope order number -->
<circle class="module" r="15" cx="556" cy="271" mask="url(#testsOrderMask)"/>
<text class="module" x="556" y="271">2</text>
<ellipse class="fixture" rx="50" ry="25" cx="331" cy="436" />
<text x="331" y="436">top</text>
<ellipse class="fixture" rx="50" ry="25" cx="331" cy="136" />
<text x="331" y="136">order</text>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,142 @@
<svg xmlns="http://www.w3.org/2000/svg" width="587" height="382">
<style>
text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
dominant-baseline: middle;
text-anchor: middle;
fill: #062886;
font-size: medium;
alignment-baseline: center;
}
ellipse.fixture, rect.test {
fill: #eeffcc;
stroke: #007020;
stroke-width: 2;
}
text.fixture {
color: #06287e;
}
circle.class, circle.module, circle.package, circle.plugin {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
text.class, text.module, text.package, text.plugin {
fill: #0e84b5;
}
line, path {
stroke: black;
stroke-width: 2;
fill: none;
}
</style>
<!-- plugin_a.py scope -->
<circle class="plugin" r="85" cx="486" cy="86" />
<!-- plugin name -->
<defs>
<path d="M 411,86 A 75 75 0 0 1 561 86" id="pluginA"/>
</defs>
<text class="plugin">
<textPath href="#pluginA" startOffset="50%">plugin_a</textPath>
</text>
<!-- scope order number -->
<mask id="pluginAOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="85" cx="486" cy="86" />
</mask>
<circle class="module" r="15" cx="571" cy="86" mask="url(#pluginAOrderMask)"/>
<text class="module" x="571" y="86">4</text>
<!-- plugin_b.py scope -->
<circle class="plugin" r="85" cx="486" cy="296" />
<!-- plugin name -->
<defs>
<path d="M 411,296 A 75 75 0 0 1 561 296" id="pluginB"/>
</defs>
<text class="plugin">
<textPath href="#pluginB" startOffset="50%">plugin_b</textPath>
</text>
<!-- scope order number -->
<mask id="pluginBOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="85" cx="486" cy="296" />
</mask>
<circle class="module" r="15" cx="571" cy="296" mask="url(#pluginBOrderMask)"/>
<text class="module" x="571" y="296">4</text>
<!-- main scope -->
<circle class="package" r="190" cx="191" cy="191" />
<!-- scope name -->
<defs>
<path d="M 11,191 A 180 180 0 0 1 371 191" id="testp"/>
</defs>
<text class="package">
<textPath href="#testp" startOffset="50%">tests</textPath>
</text>
<!-- scope order number -->
<mask id="mainOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="190" cx="191" cy="191" />
</mask>
<circle class="module" r="15" cx="381" cy="191" mask="url(#mainOrderMask)"/>
<text class="module" x="381" y="191">3</text>
<!-- subpackage -->
<circle class="package" r="140" cx="191" cy="231" />
<!-- scope name -->
<defs>
<path d="M 61,231 A 130 130 0 0 1 321 231" id="subpackage"/>
</defs>
<text class="package">
<textPath href="#subpackage" startOffset="50%">subpackage</textPath>
</text>
<!-- scope order number -->
<mask id="subpackageOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="140" cx="191" cy="231" />
</mask>
<circle class="module" r="15" cx="331" cy="231" mask="url(#subpackageOrderMask)"/>
<text class="module" x="331" y="231">2</text>
<!-- test_subpackage.py -->
<circle class="module" r="90" cx="191" cy="271" />
<!-- scope name -->
<defs>
<path d="M 111,271 A 80 80 0 0 1 271 271" id="testSubpackage"/>
</defs>
<text class="module">
<textPath href="#testSubpackage" startOffset="50%">test_subpackage.py</textPath>
</text>
<!-- scope order number -->
<mask id="testSubpackageOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="90" cx="191" cy="271" />
</mask>
<circle class="module" r="15" cx="281" cy="271" mask="url(#testSubpackageOrderMask)"/>
<text class="module" x="281" y="271">1</text>
<!-- innermost -->
<line x1="191" x2="191" y1="231" y2="311"/>
<!-- mid -->
<path d="M 191 306 L 101 306 L 91 296 L 91 156 L 101 146 L 191 146" />
<!-- order -->
<path d="M 191 316 L 91 316 L 81 306 L 81 61 L 91 51 L 191 51" />
<!-- a_fix -->
<path d="M 191 306 L 291 306 L 301 296 L 301 96 L 311 86 L 486 86" />
<!-- b_fix -->
<path d="M 191 316 L 316 316 L 336 296 L 486 296" />
<ellipse class="fixture" rx="50" ry="25" cx="191" cy="231" />
<text x="191" y="231">inner</text>
<rect class="test" width="110" height="50" x="136" y="286" />
<text x="191" y="311">test_order</text>
<ellipse class="fixture" rx="50" ry="25" cx="191" cy="146" />
<text x="191" y="146">mid</text>
<ellipse class="fixture" rx="50" ry="25" cx="191" cy="51" />
<text x="191" y="51">order</text>
<ellipse class="fixture" rx="50" ry="25" cx="486" cy="86" />
<text x="486" y="86">a_fix</text>
<ellipse class="fixture" rx="50" ry="25" cx="486" cy="296" />
<text x="486" y="296">b_fix</text>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -1,38 +0,0 @@
import pytest
# fixtures documentation order example
order = []
@pytest.fixture(scope="session")
def s1():
order.append("s1")
@pytest.fixture(scope="module")
def m1():
order.append("m1")
@pytest.fixture
def f1(f3):
order.append("f1")
@pytest.fixture
def f3():
order.append("f3")
@pytest.fixture(autouse=True)
def a1():
order.append("a1")
@pytest.fixture
def f2():
order.append("f2")
def test_order(f1, m1, f2, s1):
assert order == ["s1", "m1", "a1", "f3", "f1", "f2"]

View File

@@ -0,0 +1,45 @@
import pytest
@pytest.fixture
def order():
return []
@pytest.fixture
def a(order):
order.append("a")
@pytest.fixture
def b(a, order):
order.append("b")
@pytest.fixture(autouse=True)
def c(b, order):
order.append("c")
@pytest.fixture
def d(b, order):
order.append("d")
@pytest.fixture
def e(d, order):
order.append("e")
@pytest.fixture
def f(e, order):
order.append("f")
@pytest.fixture
def g(f, c, order):
order.append("g")
def test_order_and_g(g, order):
assert order == ["a", "b", "c", "d", "e", "f", "g"]

View File

@@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" width="252" height="682">
<style>
text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
dominant-baseline: middle;
text-anchor: middle;
fill: #062886;
font-size: medium;
}
ellipse.fixture, rect.test {
fill: #eeffcc;
stroke: #007020;
stroke-width: 2;
}
text.fixture {
color: #06287e;
}
circle.class {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
text.class {
fill: #0e84b5;
}
path, line {
stroke: black;
stroke-width: 2;
fill: none;
}
rect.autouse {
fill: #ca7f3d;
}
</style>
<path d="M126,586 L26,506 L26,236" />
<path d="M226,446 L226,236 L126,166" />
<line x1="126" x2="126" y1="656" y2="516" />
<line x1="126" x2="226" y1="516" y2="446" />
<line x1="226" x2="126" y1="446" y2="376" />
<line x1="126" x2="126" y1="376" y2="166" />
<line x1="26" x2="126" y1="236" y2="166" />
<line x1="126" x2="126" y1="166" y2="26" />
<line x1="126" x2="126" y1="96" y2="26" />
<rect class="autouse" width="251" height="40" x="0" y="286" />
<text x="126" y="306">autouse</text>
<ellipse class="fixture" rx="50" ry="25" cx="126" cy="26" />
<text x="126" y="26">order</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="96" />
<text x="126" y="96">a</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="166" />
<text x="126" y="166">b</text>
<ellipse class="fixture" rx="25" ry="25" cx="26" cy="236" />
<text x="26" y="236">c</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="376" />
<text x="126" y="376">d</text>
<ellipse class="fixture" rx="25" ry="25" cx="226" cy="446" />
<text x="226" y="446">e</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="516" />
<text x="126" y="516">f</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="586" />
<text x="126" y="586">g</text>
<rect class="test" width="110" height="50" x="71" y="631" />
<text x="126" y="656">test_order</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,31 @@
import pytest
@pytest.fixture(scope="class")
def order():
return []
@pytest.fixture(scope="class", autouse=True)
def c1(order):
order.append("c1")
@pytest.fixture(scope="class")
def c2(order):
order.append("c2")
@pytest.fixture(scope="class")
def c3(order, c1):
order.append("c3")
class TestClassWithC1Request:
def test_order(self, order, c1, c3):
assert order == ["c1", "c3"]
class TestClassWithoutC1Request:
def test_order(self, order, c2):
assert order == ["c1", "c2"]

View File

@@ -0,0 +1,76 @@
<svg xmlns="http://www.w3.org/2000/svg" width="862" height="402">
<style>
text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
dominant-baseline: middle;
text-anchor: middle;
fill: #062886;
font-size: medium;
}
ellipse.fixture, rect.test {
fill: #eeffcc;
stroke: #007020;
stroke-width: 2;
}
text.fixture {
color: #06287e;
}
circle.class {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
text.class {
fill: #0e84b5;
}
line {
stroke: black;
stroke-width: 2;
}
rect.autouse {
fill: #ca7f3d;
}
</style>
<!-- TestWithC1Request -->
<circle class="class" r="200" cx="221" cy="201" />
<line x1="221" x2="221" y1="61" y2="316"/>
<ellipse class="fixture" rx="50" ry="25" cx="221" cy="61" />
<text x="221" y="61">order</text>
<ellipse class="fixture" rx="25" ry="25" cx="221" cy="131" />
<text x="221" y="131">c1</text>
<ellipse class="fixture" rx="25" ry="25" cx="221" cy="271" />
<text x="221" y="271">c3</text>
<rect class="test" width="110" height="50" x="166" y="316" />
<text x="221" y="341">test_order</text>
<!-- scope name -->
<defs>
<path d="M31,201 A 190 190 0 0 1 411 201" id="testClassWith"/>
</defs>
<text class="class">
<textPath href="#testClassWith" startOffset="50%">TestWithC1Request</textPath>
</text>
<!-- TestWithoutC1Request -->
<circle class="class" r="200" cx="641" cy="201" />
<line x1="641" x2="641" y1="61" y2="316"/>
<ellipse class="fixture" rx="50" ry="25" cx="641" cy="61" />
<text x="641" y="61">order</text>
<ellipse class="fixture" rx="25" ry="25" cx="641" cy="131" />
<text x="641" y="131">c1</text>
<ellipse class="fixture" rx="25" ry="25" cx="641" cy="271" />
<text x="641" y="271">c2</text>
<rect class="test" width="110" height="50" x="586" y="316" />
<text x="641" y="341">test_order</text>
<!-- scope name -->
<defs>
<path d="M451,201 A 190 190 0 0 1 831 201" id="testClassWithout"/>
</defs>
<text class="class">
<textPath href="#testClassWithout" startOffset="50%">TestWithoutC1Request</textPath>
</text>
<rect class="autouse" width="862" height="40" x="1" y="181" />
<rect width="10" height="100" class="autouse" x="426" y="151" />
<text x="431" y="201">autouse</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,36 @@
import pytest
@pytest.fixture
def order():
return []
@pytest.fixture
def c1(order):
order.append("c1")
@pytest.fixture
def c2(order):
order.append("c2")
class TestClassWithAutouse:
@pytest.fixture(autouse=True)
def c3(self, order, c2):
order.append("c3")
def test_req(self, order, c1):
assert order == ["c2", "c3", "c1"]
def test_no_req(self, order):
assert order == ["c2", "c3"]
class TestClassWithoutAutouse:
def test_req(self, order, c1):
assert order == ["c1"]
def test_no_req(self, order):
assert order == []

View File

@@ -0,0 +1,100 @@
<svg xmlns="http://www.w3.org/2000/svg" width="862" height="502">
<style>
text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
dominant-baseline: middle;
text-anchor: middle;
fill: #062886;
font-size: medium;
}
ellipse.fixture, rect.test {
fill: #eeffcc;
stroke: #007020;
stroke-width: 2;
}
text.fixture {
color: #06287e;
}
circle.class {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
text.class {
fill: #0e84b5;
}
line {
stroke: black;
stroke-width: 2;
}
rect.autouse {
fill: #ca7f3d;
}
</style>
<!-- TestWithAutouse -->
<circle class="class" r="250" cx="251" cy="251" />
<!-- scope name -->
<defs>
<path d="M11,251 A 240 240 0 0 1 491 251" id="testClassWith"/>
</defs>
<text class="class">
<textPath href="#testClassWith" startOffset="50%">TestWithAutouse</textPath>
</text>
<mask id="autouseScope">
<circle fill="white" r="249" cx="251" cy="251" />
</mask>
<!-- TestWithAutouse.test_req -->
<line x1="176" x2="176" y1="76" y2="426"/>
<ellipse class="fixture" rx="50" ry="25" cx="176" cy="76" />
<text x="176" y="76">order</text>
<ellipse class="fixture" rx="25" ry="25" cx="176" cy="146" />
<text x="176" y="146">c2</text>
<ellipse class="fixture" rx="25" ry="25" cx="176" cy="216" />
<text x="176" y="216">c3</text>
<ellipse class="fixture" rx="25" ry="25" cx="176" cy="356" />
<text x="176" y="356">c1</text>
<rect class="test" width="100" height="50" x="126" y="401" />
<text x="176" y="426">test_req</text>
<!-- TestWithAutouse.test_no_req -->
<line x1="326" x2="326" y1="76" y2="346"/>
<ellipse class="fixture" rx="50" ry="25" cx="326" cy="76" />
<text x="326" y="76">order</text>
<ellipse class="fixture" rx="25" ry="25" cx="326" cy="146" />
<text x="326" y="146">c2</text>
<ellipse class="fixture" rx="25" ry="25" cx="326" cy="216" />
<text x="326" y="216">c3</text>
<rect class="test" width="120" height="50" x="266" y="331" />
<text x="326" y="356">test_no_req</text>
<rect class="autouse" width="500" height="40" x="1" y="266" mask="url(#autouseScope)"/>
<text x="261" y="286">autouse</text>
<!-- TestWithoutAutouse -->
<circle class="class" r="170" cx="691" cy="251" />
<!-- scope name -->
<defs>
<path d="M 531,251 A 160 160 0 0 1 851 251" id="testClassWithout"/>
</defs>
<text class="class">
<textPath href="#testClassWithout" startOffset="50%">TestWithoutAutouse</textPath>
</text>
<!-- TestWithoutAutouse.test_req -->
<line x1="616" x2="616" y1="181" y2="321"/>
<ellipse class="fixture" rx="50" ry="25" cx="616" cy="181" />
<text x="616" y="181">order</text>
<ellipse class="fixture" rx="25" ry="25" cx="616" cy="251" />
<text x="616" y="251">c1</text>
<rect class="test" width="100" height="50" x="566" y="296" />
<text x="616" y="321">test_req</text>
<!-- TestWithoutAutouse.test_no_req -->
<line x1="766" x2="766" y1="181" y2="251"/>
<ellipse class="fixture" rx="50" ry="25" cx="766" cy="181" />
<text x="766" y="181">order</text>
<rect class="test" width="120" height="50" x="706" y="226" />
<text x="766" y="251">test_no_req</text>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,45 @@
import pytest
@pytest.fixture
def order():
return []
@pytest.fixture
def a(order):
order.append("a")
@pytest.fixture
def b(a, order):
order.append("b")
@pytest.fixture
def c(a, b, order):
order.append("c")
@pytest.fixture
def d(c, b, order):
order.append("d")
@pytest.fixture
def e(d, b, order):
order.append("e")
@pytest.fixture
def f(e, order):
order.append("f")
@pytest.fixture
def g(f, c, order):
order.append("g")
def test_order(g, order):
assert order == ["a", "b", "c", "d", "e", "f", "g"]

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="252" height="612">
<style>
text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
dominant-baseline: middle;
text-anchor: middle;
fill: #062886;
font-size: medium;
}
ellipse.fixture, rect.test {
fill: #eeffcc;
stroke: #007020;
stroke-width: 2;
}
text.fixture {
color: #06287e;
}
circle.class {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
text.class {
fill: #0e84b5;
}
path, line {
stroke: black;
stroke-width: 2;
fill: none;
}
</style>
<path d="M126,516 L26,436 L26,236" />
<path d="M226,376 L226,236 L126,166" />
<line x1="126" x2="126" y1="586" y2="446" />
<line x1="126" x2="226" y1="446" y2="376" />
<line x1="226" x2="126" y1="376" y2="306" />
<line x1="126" x2="26" y1="306" y2="236" />
<line x1="126" x2="126" y1="306" y2="166" />
<line x1="26" x2="126" y1="236" y2="166" />
<line x1="126" x2="126" y1="166" y2="26" />
<line x1="126" x2="126" y1="96" y2="26" />
<ellipse class="fixture" rx="50" ry="25" cx="126" cy="26" />
<text x="126" y="26">order</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="96" />
<text x="126" y="96">a</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="166" />
<text x="126" y="166">b</text>
<ellipse class="fixture" rx="25" ry="25" cx="26" cy="236" />
<text x="26" y="236">c</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="306" />
<text x="126" y="306">d</text>
<ellipse class="fixture" rx="25" ry="25" cx="226" cy="376" />
<text x="226" y="376">e</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="446" />
<text x="126" y="446">f</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="516" />
<text x="126" y="516">g</text>
<rect class="test" width="110" height="50" x="71" y="561" />
<text x="126" y="586">test_order</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" width="112" height="612">
<style>
text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
dominant-baseline: middle;
text-anchor: middle;
fill: #062886;
font-size: medium;
}
ellipse.fixture, rect.test {
fill: #eeffcc;
stroke: #007020;
stroke-width: 2;
}
text.fixture {
color: #06287e;
}
circle.class {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
text.class {
fill: #0e84b5;
}
path, line {
stroke: black;
stroke-width: 2;
fill: none;
}
</style>
<line x1="56" x2="56" y1="611" y2="26" />
<ellipse class="fixture" rx="50" ry="25" cx="56" cy="26" />
<text x="56" y="26">order</text>
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="96" />
<text x="56" y="96">a</text>
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="166" />
<text x="56" y="166">b</text>
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="236" />
<text x="56" y="236">c</text>
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="306" />
<text x="56" y="306">d</text>
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="376" />
<text x="56" y="376">e</text>
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="446" />
<text x="56" y="446">f</text>
<ellipse class="fixture" rx="25" ry="25" cx="56" cy="516" />
<text x="56" y="516">g</text>
<rect class="test" width="110" height="50" x="1" y="561" />
<text x="56" y="586">test_order</text>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="252" height="542">
<style>
text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
dominant-baseline: middle;
text-anchor: middle;
fill: #062886;
font-size: medium;
}
ellipse.fixture, rect.test {
fill: #eeffcc;
stroke: #007020;
stroke-width: 2;
}
text.fixture {
color: #06287e;
}
circle.class {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
text.class {
fill: #0e84b5;
}
path, line {
stroke: black;
stroke-width: 2;
fill: none;
}
</style>
<path d="M126,446 L26,376 L26,236" />
<path d="M226,306 L126,236 L126,166" />
<line x1="126" x2="126" y1="516" y2="446" />
<line x1="226" x2="226" y1="376" y2="306" />
<line x1="226" x2="226" y1="306" y2="236" />
<line x1="226" x2="126" y1="236" y2="166" />
<line x1="126" x2="226" y1="446" y2="376" />
<line x1="26" x2="126" y1="236" y2="166" />
<line x1="126" x2="126" y1="166" y2="96" />
<line x1="126" x2="126" y1="96" y2="26" />
<ellipse class="fixture" rx="50" ry="25" cx="126" cy="26" />
<text x="126" y="26">order</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="96" />
<text x="126" y="96">a</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="166" />
<text x="126" y="166">b</text>
<ellipse class="fixture" rx="25" ry="25" cx="26" cy="236" />
<text x="26" y="236">c</text>
<ellipse class="fixture" rx="25" ry="25" cx="226" cy="236" />
<text x="226" y="236">d</text>
<ellipse class="fixture" rx="25" ry="25" cx="226" cy="306" />
<text x="226" y="306">e</text>
<ellipse class="fixture" rx="25" ry="25" cx="226" cy="376" />
<text x="226" y="376">f</text>
<ellipse class="fixture" rx="25" ry="25" cx="126" cy="446" />
<text x="126" y="446">g</text>
<rect class="test" width="110" height="50" x="71" y="491" />
<text x="126" y="516">test_order</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,36 @@
import pytest
@pytest.fixture(scope="session")
def order():
return []
@pytest.fixture
def func(order):
order.append("function")
@pytest.fixture(scope="class")
def cls(order):
order.append("class")
@pytest.fixture(scope="module")
def mod(order):
order.append("module")
@pytest.fixture(scope="package")
def pack(order):
order.append("package")
@pytest.fixture(scope="session")
def sess(order):
order.append("session")
class TestClass:
def test_order(self, func, cls, mod, pack, sess, order):
assert order == ["session", "package", "module", "class", "function"]

View File

@@ -0,0 +1,55 @@
<svg xmlns="http://www.w3.org/2000/svg" width="262" height="537">
<style>
text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
dominant-baseline: middle;
text-anchor: middle;
fill: #062886;
font-size: medium;
}
ellipse.fixture, rect.test {
fill: #eeffcc;
stroke: #007020;
stroke-width: 2;
}
text.fixture {
color: #06287e;
}
circle.class {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
text.class {
fill: #0e84b5;
}
line {
stroke: black;
stroke-width: 2;
}
</style>
<!-- TestClass -->
<circle class="class" r="130" cx="131" cy="406" />
<line x1="131" x2="131" y1="21" y2="446"/>
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="26" />
<text x="131" y="26">order</text>
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="96" />
<text x="131" y="96">sess</text>
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="166" />
<text x="131" y="166">pack</text>
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="236" />
<text x="131" y="236">mod</text>
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="306" />
<text x="131" y="306">cls</text>
<ellipse class="fixture" rx="50" ry="25" cx="131" cy="376" />
<text x="131" y="376">func</text>
<rect class="test" width="110" height="50" x="76" y="421" />
<text x="131" y="446">test_order</text>
<!-- scope name -->
<defs>
<path d="M131,526 A 120 120 0 0 1 136 286" id="testClass"/>
</defs>
<text class="class">
<textPath href="#testClass" startOffset="50%">TestClass</textPath>
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,29 @@
import pytest
@pytest.fixture
def order():
return []
@pytest.fixture
def outer(order, inner):
order.append("outer")
class TestOne:
@pytest.fixture
def inner(self, order):
order.append("one")
def test_order(self, order, outer):
assert order == ["one", "outer"]
class TestTwo:
@pytest.fixture
def inner(self, order):
order.append("two")
def test_order(self, order, outer):
assert order == ["two", "outer"]

View File

@@ -0,0 +1,115 @@
<svg xmlns="http://www.w3.org/2000/svg" width="562" height="532">
<style>
text {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
dominant-baseline: middle;
text-anchor: middle;
fill: #062886;
font-size: medium;
}
ellipse.fixture, rect.test {
fill: #eeffcc;
stroke: #007020;
stroke-width: 2;
}
text.fixture {
color: #06287e;
}
circle.class {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
circle.module {
fill: #c3e0ec;
stroke: #0e84b5;
stroke-width: 2;
}
text.class, text.module {
fill: #0e84b5;
}
line, path {
stroke: black;
stroke-width: 2;
fill: none;
}
</style>
<!-- main scope -->
<circle class="module" r="265" cx="281" cy="266" />
<!-- scope name -->
<defs>
<path d="M 26,266 A 255 255 0 0 1 536 266" id="testModule"/>
</defs>
<text class="module">
<textPath href="#testModule" startOffset="50%">test_fixtures_request_different_scope.py</textPath>
</text>
<!-- TestOne -->
<circle class="class" r="100" cx="141" cy="266" />
<!-- inner -->
<line x1="141" x2="141" y1="231" y2="301"/>
<!-- order -->
<path d="M 141 296 L 201 296 L 211 286 L 211 146 L 221 136 L 281 136" />
<!-- outer -->
<path d="M 141 306 L 201 306 L 211 316 L 211 386 L 221 396 L 281 396" />
<ellipse class="fixture" rx="50" ry="25" cx="141" cy="231" />
<text x="141" y="231">inner</text>
<rect class="test" width="110" height="50" x="86" y="276" />
<text x="141" y="301">test_order</text>
<!-- scope name -->
<defs>
<path d="M 51,266 A 90 90 0 0 1 231 266" id="testOne"/>
</defs>
<text class="class">
<textPath href="#testOne" startOffset="50%">TestOne</textPath>
</text>
<!-- scope order number -->
<mask id="testOneOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="100" cx="141" cy="266" />
</mask>
<circle class="module" r="15" cx="41" cy="266" mask="url(#testOneOrderMask)"/>
<text class="module" x="41" y="266">1</text>
<!-- scope order number -->
<mask id="testMainOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="265" cx="281" cy="266" />
</mask>
<circle class="module" r="15" cx="16" cy="266" mask="url(#testMainOrderMask)"/>
<text class="module" x="16" y="266">2</text>
<!-- TestTwo -->
<circle class="class" r="100" cx="421" cy="266" />
<!-- inner -->
<line x1="421" x2="421" y1="231" y2="301"/>
<!-- order -->
<path d="M 421 296 L 361 296 L 351 286 L 351 146 L 341 136 L 281 136" />
<!-- outer -->
<path d="M 421 306 L 361 306 L 351 316 L 351 386 L 341 396 L 281 396" />
<ellipse class="fixture" rx="50" ry="25" cx="421" cy="231" />
<text x="421" y="231">inner</text>
<rect class="test" width="110" height="50" x="366" y="276" />
<text x="421" y="301">test_order</text>
<!-- scope name -->
<defs>
<path d="M 331,266 A 90 90 0 0 1 511 266" id="testTwo"/>
</defs>
<text class="class">
<textPath href="#testTwo" startOffset="50%">TestTwo</textPath>
</text>
<!-- scope order number -->
<mask id="testTwoOrderMask">
<rect x="0" y="0" width="100%" height="100%" fill="white"/>
<circle fill="black" stroke="white" stroke-width="2" r="100" cx="421" cy="266" />
</mask>
<circle class="module" r="15" cx="521" cy="266" mask="url(#testTwoOrderMask)"/>
<text class="module" x="521" y="266">1</text>
<!-- scope order number -->
<circle class="module" r="15" cx="546" cy="266" mask="url(#testMainOrderMask)"/>
<text class="module" x="546" y="266">2</text>
<ellipse class="fixture" rx="50" ry="25" cx="281" cy="396" />
<text x="281" y="396">outer</text>
<ellipse class="fixture" rx="50" ry="25" cx="281" cy="136" />
<text x="281" y="136">order</text>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -221,14 +221,19 @@ Registering markers for your test suite is simple:
[pytest]
markers =
webtest: mark a test as a webtest.
slow: mark test as slow.
You can ask which markers exist for your test suite - the list includes our just defined ``webtest`` markers:
Multiple custom markers can be registered, by defining each one in its own line, as shown in above example.
You can ask which markers exist for your test suite - the list includes our just defined ``webtest`` and ``slow`` markers:
.. code-block:: pytest
$ pytest --markers
@pytest.mark.webtest: mark a test as a webtest.
@pytest.mark.slow: mark test as slow.
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/warnings.html#pytest-mark-filterwarnings
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
@@ -539,7 +544,7 @@ Let's run this without capturing output and see what we get:
.
1 passed in 0.12s
marking platform specific tests with pytest
Marking platform specific tests with pytest
--------------------------------------------------------------
.. regendoc:wipe

View File

@@ -26,7 +26,7 @@ class Python:
def __init__(self, version, picklefile):
self.pythonpath = shutil.which(version)
if not self.pythonpath:
pytest.skip("{!r} not found".format(version))
pytest.skip(f"{version!r} not found")
self.picklefile = picklefile
def dumps(self, obj):
@@ -69,4 +69,4 @@ class Python:
@pytest.mark.parametrize("obj", [42, {}, {1: 3}])
def test_basic_objects(python1, python2, obj):
python1.dumps(obj)
python2.load_and_is_true("obj == {}".format(obj))
python2.load_and_is_true(f"obj == {obj}")

View File

@@ -102,4 +102,4 @@ interesting to just look at the collection tree:
<YamlItem hello>
<YamlItem ok>
========================== no tests ran in 0.12s ===========================
======================== 2 tests collected in 0.12s ========================

View File

@@ -40,7 +40,7 @@ class YamlItem(pytest.Item):
)
def reportinfo(self):
return self.fspath, 0, "usecase: {}".format(self.name)
return self.fspath, 0, f"usecase: {self.name}"
class YamlException(Exception):

View File

@@ -175,7 +175,7 @@ objects, they are still using the default pytest representation:
<Function test_timedistance_v3[forward]>
<Function test_timedistance_v3[backward]>
========================== no tests ran in 0.12s ===========================
======================== 8 tests collected in 0.12s ========================
In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs
together with the actual data, instead of listing them separately.
@@ -252,7 +252,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
<Function test_demo1[advanced]>
<Function test_demo2[advanced]>
========================== no tests ran in 0.12s ===========================
======================== 4 tests collected in 0.12s ========================
Note that we told ``metafunc.parametrize()`` that your scenario values
should be considered class-scoped. With pytest-2.3 this leads to a
@@ -328,7 +328,7 @@ Let's first see how it looks like at collection time:
<Function test_db_initialized[d1]>
<Function test_db_initialized[d2]>
========================== no tests ran in 0.12s ===========================
======================== 2 tests collected in 0.12s ========================
And then when we run the test:
@@ -637,13 +637,13 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker:
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collecting ... collected 14 items / 11 deselected / 3 selected
collecting ... collected 24 items / 21 deselected / 3 selected
test_pytest_param_example.py::test_eval[1+7-8] PASSED [ 33%]
test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%]
test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%]
=============== 2 passed, 11 deselected, 1 xfailed in 0.12s ================
=============== 2 passed, 21 deselected, 1 xfailed in 0.12s ================
As the result:

View File

@@ -157,7 +157,7 @@ The test collection would look like this:
<Function simple_check>
<Function complex_check>
========================== no tests ran in 0.12s ===========================
======================== 2 tests collected in 0.12s ========================
You can check for multiple glob patterns by adding a space between the patterns:
@@ -220,7 +220,7 @@ You can always peek at the collection tree without running tests like this:
<Function test_method>
<Function test_anothermethod>
========================== no tests ran in 0.12s ===========================
======================== 3 tests collected in 0.12s ========================
.. _customizing-test-collection:
@@ -282,7 +282,7 @@ leave out the ``setup.py`` file:
<Module 'pkg/module_py2.py'>
<Function 'test_only_on_python2'>
====== no tests ran in 0.04 seconds ======
====== 1 tests found in 0.04 seconds ======
If you run with a Python 3 interpreter both the one test and the ``setup.py``
file will be left out:
@@ -296,7 +296,7 @@ file will be left out:
rootdir: $REGENDOC_TMPDIR, configfile: pytest.ini
collected 0 items
========================== no tests ran in 0.12s ===========================
======================= no tests collected in 0.12s ========================
It's also possible to ignore files based on Unix shell-style wildcards by adding
patterns to :globalvar:`collect_ignore_glob`.

View File

@@ -446,7 +446,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
items = [1, 2, 3]
print("items is {!r}".format(items))
print(f"items is {items!r}")
> a, b = items.pop()
E TypeError: cannot unpack non-iterable int object

View File

@@ -452,7 +452,7 @@ and nothing when run plainly:
========================== no tests ran in 0.12s ===========================
profiling test duration
Profiling test duration
--------------------------
.. regendoc:wipe
@@ -498,7 +498,7 @@ Now we can profile which test functions execute the slowest:
0.10s call test_some_are_slow.py::test_funcfast
============================ 3 passed in 0.12s =============================
incremental testing - test steps
Incremental testing - test steps
---------------------------------------------------
.. regendoc:wipe
@@ -739,7 +739,7 @@ it (unless you use "autouse" fixture which are always executed ahead of the firs
executing).
post-process test reports / failures
Post-process test reports / failures
---------------------------------------
If you want to postprocess test reports and need access to the executing

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
Installation and Getting Started
===================================
**Pythons**: Python 3.5, 3.6, 3.7, 3.8, 3.9, PyPy3
**Pythons**: Python 3.6, 3.7, 3.8, 3.9, PyPy3
**Platforms**: Linux and Windows
@@ -28,7 +28,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 6.1.0
pytest 6.2.2
.. _`simpletest`:

View File

@@ -2,7 +2,7 @@
.. sidebar:: Next Open Trainings
- `Professional testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via Python Academy, February 1-3 2021, Leipzig (Germany) and remote.
- `Professional testing with Python <https://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_, via Python Academy, February 1-3 2021, remote and Leipzig (Germany). **Early-bird discount available until January 15th**.
Also see `previous talks and blogposts <talks.html>`_.
@@ -69,7 +69,7 @@ Features
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
- Python 3.5+ and PyPy 3
- Python 3.6+ and PyPy 3
- Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community

View File

@@ -68,4 +68,13 @@ Unsupported idioms / known issues
fundamentally incompatible with pytest because they don't support fixtures
properly since collection and test execution are separated.
Migrating from nose to pytest
------------------------------
`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script
and pytest plugin to help convert Nose-based tests into pytest-based tests.
Specifically, the script transforms nose.tools.assert_* function calls into
raw assert statements, while preserving format of original arguments
as much as possible.
.. _nose: https://nose.readthedocs.io/en/latest/

View File

@@ -3,6 +3,8 @@
API Reference
=============
.. module:: pytest
This page contains the full reference to pytest's API.
.. contents::
@@ -189,7 +191,7 @@ Mark a test function as using the given fixture names.
When using `usefixtures` in hooks, it can only load fixtures when applied to a test function before test setup
(for example in the `pytest_collection_modifyitems` hook).
Also not that his mark has no effect when applied to **fixtures**.
Also note that this mark has no effect when applied to **fixtures**.
@@ -248,6 +250,16 @@ Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to
mark.args == (10, "slow")
mark.kwargs == {"method": "thread"}
Example for using multiple custom markers:
.. code-block:: python
@pytest.mark.timeout(10, "slow", method="thread")
@pytest.mark.slow
def test_function():
...
When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``.
.. _`fixtures-api`:
@@ -304,11 +316,10 @@ request ``pytestconfig`` into your fixture and get it with ``pytestconfig.cache`
Under the hood, the cache plugin uses the simple
``dumps``/``loads`` API of the :py:mod:`json` stdlib module.
.. currentmodule:: _pytest.cacheprovider
``config.cache`` is an instance of :class:`pytest.Cache`:
.. automethod:: Cache.get
.. automethod:: Cache.set
.. automethod:: Cache.makedir
.. autoclass:: pytest.Cache()
:members:
.. fixture:: capsys
@@ -318,12 +329,10 @@ capsys
**Tutorial**: :doc:`capture`.
.. currentmodule:: _pytest.capture
.. autofunction:: capsys()
.. autofunction:: _pytest.capture.capsys()
:no-auto-options:
Returns an instance of :py:class:`CaptureFixture`.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
@@ -334,7 +343,7 @@ capsys
captured = capsys.readouterr()
assert captured.out == "hello\n"
.. autoclass:: CaptureFixture()
.. autoclass:: pytest.CaptureFixture()
:members:
@@ -345,10 +354,10 @@ capsysbinary
**Tutorial**: :doc:`capture`.
.. autofunction:: capsysbinary()
.. autofunction:: _pytest.capture.capsysbinary()
:no-auto-options:
Returns an instance of :py:class:`CaptureFixture`.
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
@@ -367,10 +376,10 @@ capfd
**Tutorial**: :doc:`capture`.
.. autofunction:: capfd()
.. autofunction:: _pytest.capture.capfd()
:no-auto-options:
Returns an instance of :py:class:`CaptureFixture`.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
@@ -389,10 +398,10 @@ capfdbinary
**Tutorial**: :doc:`capture`.
.. autofunction:: capfdbinary()
.. autofunction:: _pytest.capture.capfdbinary()
:no-auto-options:
Returns an instance of :py:class:`CaptureFixture`.
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
@@ -433,7 +442,7 @@ request
The ``request`` fixture is a special fixture providing information of the requesting test function.
.. autoclass:: _pytest.fixtures.FixtureRequest()
.. autoclass:: pytest.FixtureRequest()
:members:
@@ -475,9 +484,9 @@ caplog
.. autofunction:: _pytest.logging.caplog()
:no-auto-options:
Returns a :class:`_pytest.logging.LogCaptureFixture` instance.
Returns a :class:`pytest.LogCaptureFixture` instance.
.. autoclass:: _pytest.logging.LogCaptureFixture
.. autoclass:: pytest.LogCaptureFixture()
:members:
@@ -486,30 +495,30 @@ caplog
monkeypatch
~~~~~~~~~~~
.. currentmodule:: _pytest.monkeypatch
**Tutorial**: :doc:`monkeypatch`.
.. autofunction:: _pytest.monkeypatch.monkeypatch()
:no-auto-options:
Returns a :class:`MonkeyPatch` instance.
Returns a :class:`~pytest.MonkeyPatch` instance.
.. autoclass:: _pytest.monkeypatch.MonkeyPatch
.. autoclass:: pytest.MonkeyPatch
:members:
.. fixture:: testdir
.. fixture:: pytester
testdir
~~~~~~~
pytester
~~~~~~~~
.. currentmodule:: _pytest.pytester
.. versionadded:: 6.2
This fixture provides a :class:`Testdir` instance useful for black-box testing of test files, making it ideal to
test plugins.
Provides a :class:`~pytest.Pytester` instance that can be used to run and test pytest itself.
To use it, include in your top-most ``conftest.py`` file:
It provides an empty directory where pytest can be executed in isolation, and contains facilities
to write tests, configuration files, and match against expected output.
To use it, include in your topmost ``conftest.py`` file:
.. code-block:: python
@@ -517,13 +526,30 @@ To use it, include in your top-most ``conftest.py`` file:
.. autoclass:: Testdir()
.. autoclass:: pytest.Pytester()
:members:
.. autoclass:: RunResult()
.. autoclass:: _pytest.pytester.RunResult()
:members:
.. autoclass:: LineMatcher()
.. autoclass:: _pytest.pytester.LineMatcher()
:members:
:special-members: __str__
.. autoclass:: _pytest.pytester.HookRecorder()
:members:
.. fixture:: testdir
testdir
~~~~~~~
Identical to :fixture:`pytester`, but provides an instance whose methods return
legacy ``py.path.local`` objects instead when applicable.
New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
.. autoclass:: pytest.Testdir()
:members:
@@ -534,12 +560,10 @@ recwarn
**Tutorial**: :ref:`assertwarnings`
.. currentmodule:: _pytest.recwarn
.. autofunction:: recwarn()
.. autofunction:: _pytest.recwarn.recwarn()
:no-auto-options:
.. autoclass:: WarningsRecorder()
.. autoclass:: pytest.WarningsRecorder()
:members:
Each recorded warning is an instance of :class:`warnings.WarningMessage`.
@@ -556,13 +580,11 @@ tmp_path
**Tutorial**: :doc:`tmpdir`
.. currentmodule:: _pytest.tmpdir
.. autofunction:: tmp_path()
.. autofunction:: _pytest.tmpdir.tmp_path()
:no-auto-options:
.. fixture:: tmp_path_factory
.. fixture:: _pytest.tmpdir.tmp_path_factory
tmp_path_factory
~~~~~~~~~~~~~~~~
@@ -571,12 +593,9 @@ tmp_path_factory
.. _`tmp_path_factory factory api`:
``tmp_path_factory`` instances have the following methods:
``tmp_path_factory`` is an instance of :class:`~pytest.TempPathFactory`:
.. currentmodule:: _pytest.tmpdir
.. automethod:: TempPathFactory.mktemp
.. automethod:: TempPathFactory.getbasetemp
.. autoclass:: pytest.TempPathFactory()
.. fixture:: tmpdir
@@ -586,9 +605,7 @@ tmpdir
**Tutorial**: :doc:`tmpdir`
.. currentmodule:: _pytest.tmpdir
.. autofunction:: tmpdir()
.. autofunction:: _pytest.tmpdir.tmpdir()
:no-auto-options:
@@ -601,12 +618,9 @@ tmpdir_factory
.. _`tmpdir factory api`:
``tmpdir_factory`` instances have the following methods:
``tmp_path_factory`` is an instance of :class:`~pytest.TempdirFactory`:
.. currentmodule:: _pytest.tmpdir
.. automethod:: TempdirFactory.mktemp
.. automethod:: TempdirFactory.getbasetemp
.. autoclass:: pytest.TempdirFactory()
.. _`hook-reference`:
@@ -668,6 +682,10 @@ items, delete or otherwise amend the test items:
.. autofunction:: pytest_collection_modifyitems
.. note::
If this hook is implemented in ``conftest.py`` files, it always receives all collected items, not only those
under the ``conftest.py`` where it is implemented.
.. autofunction:: pytest_collection_finish
Test running (runtest) hooks
@@ -816,6 +834,13 @@ Function
:members:
:show-inheritance:
FunctionDefinition
~~~~~~~~~~~~~~~~~~
.. autoclass:: _pytest.python.FunctionDefinition()
:members:
:show-inheritance:
Item
~~~~
@@ -1061,6 +1086,12 @@ Custom warnings generated in some situations such as improper usage or deprecate
.. autoclass:: pytest.PytestUnknownMarkWarning
:show-inheritance:
.. autoclass:: pytest.PytestUnraisableExceptionWarning
:show-inheritance:
.. autoclass:: pytest.PytestUnhandledThreadExceptionWarning
:show-inheritance:
Consult the :ref:`internal-warnings` section in the documentation for more information.
@@ -1236,12 +1267,13 @@ passed multiple times. The expected format is ``name=value``. For example::
.. confval:: junit_family
.. versionadded:: 4.2
.. versionchanged:: 6.1
Default changed to ``xunit2``.
Configures the format of the generated JUnit XML file. The possible options are:
* ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format. **This is the default**.
* ``xunit2``: produces `xunit 2.0 style output <https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd>`__,
which should be more compatible with latest Jenkins versions.
* ``xunit1`` (or ``legacy``): produces old style output, compatible with the xunit 1.0 format.
* ``xunit2``: produces `xunit 2.0 style output <https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd>`__, which should be more compatible with latest Jenkins versions. **This is the default**.
.. code-block:: ini
@@ -1511,7 +1543,8 @@ passed multiple times. The expected format is ``name=value``. For example::
[seq] matches any character in seq
[!seq] matches any char not in seq
Default patterns are ``'.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv'``.
Default patterns are ``'*.egg'``, ``'.*'``, ``'_darcs'``, ``'build'``,
``'CVS'``, ``'dist'``, ``'node_modules'``, ``'venv'``, ``'{arch}'``.
Setting a ``norecursedirs`` replaces the default. Here is an example of
how to avoid certain directories:
@@ -1718,7 +1751,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
failures.
--sw, --stepwise exit on test failure and continue from last failing
test next time
--stepwise-skip ignore the first failing test but stop on the next
--sw-skip, --stepwise-skip
ignore the first failing test but stop on the next
failing test
reporting:
@@ -1760,9 +1794,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
--maxfail=num exit after first num failures or errors.
--strict-config any warnings encountered while parsing the `pytest`
section of the configuration file raise errors.
--strict-markers, --strict
markers not registered in the `markers` section of
--strict-markers markers not registered in the `markers` section of
the configuration file raise errors.
--strict (deprecated) alias to --strict-markers.
-c file load configuration from `file` instead of trying to
locate one of the implicit configuration files.
--continue-on-collection-errors

View File

@@ -91,7 +91,7 @@ when run on an interpreter earlier than Python3.6:
import sys
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher")
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_function():
...
@@ -259,7 +259,7 @@ These two examples illustrate situations where you don't want to check for a con
at the module level, which is when a condition would otherwise be evaluated for marks.
This will make ``test_function`` ``XFAIL``. Note that no other code is executed after
the ``pytest.xfail`` call, differently from the marker. That's because it is implemented
the :func:`pytest.xfail` call, differently from the marker. That's because it is implemented
internally by raising a known exception.
**Reference**: :ref:`pytest.mark.xfail ref`
@@ -358,7 +358,7 @@ By specifying on the commandline:
pytest --runxfail
you can force the running and reporting of an ``xfail`` marked test
as if it weren't marked at all. This also causes ``pytest.xfail`` to produce no effect.
as if it weren't marked at all. This also causes :func:`pytest.xfail` to produce no effect.
Examples
~~~~~~~~

View File

@@ -15,13 +15,11 @@ You can use the ``tmp_path`` fixture which will
provide a temporary directory unique to the test invocation,
created in the `base temporary directory`_.
``tmp_path`` is a ``pathlib/pathlib2.Path`` object. Here is an example test usage:
``tmp_path`` is a ``pathlib.Path`` object. Here is an example test usage:
.. code-block:: python
# content of test_tmp_path.py
import os
CONTENT = "content"
@@ -63,7 +61,7 @@ Running this would result in a passed test except for the last
> assert 0
E assert 0
test_tmp_path.py:13: AssertionError
test_tmp_path.py:11: AssertionError
========================= short test summary info ==========================
FAILED test_tmp_path.py::test_create_file - assert 0
============================ 1 failed in 0.12s =============================
@@ -97,9 +95,6 @@ and more. Here is an example test usage:
.. code-block:: python
# content of test_tmpdir.py
import os
def test_create_file(tmpdir):
p = tmpdir.mkdir("sub").join("hello.txt")
p.write("content")
@@ -134,7 +129,7 @@ Running this would result in a passed test except for the last
> assert 0
E assert 0
test_tmpdir.py:9: AssertionError
test_tmpdir.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_tmpdir.py::test_create_file - assert 0
============================ 1 failed in 0.12s =============================

View File

@@ -470,6 +470,38 @@ seconds to finish (not available on Windows).
the command-line using ``-o faulthandler_timeout=X``.
.. _unraisable:
Warning about unraisable exceptions and unhandled thread exceptions
-------------------------------------------------------------------
.. versionadded:: 6.2
.. note::
These features only work on Python>=3.8.
Unhandled exceptions are exceptions that are raised in a situation in which
they cannot propagate to a caller. The most common case is an exception raised
in a :meth:`__del__ <object.__del__>` implementation.
Unhandled thread exceptions are exceptions raised in a :class:`~threading.Thread`
but not handled, causing the thread to terminate uncleanly.
Both types of exceptions are normally considered bugs, but may go unnoticed
because they don't cause the program itself to crash. Pytest detects these
conditions and issues a warning that is visible in the test run summary.
The plugins are automatically enabled for pytest runs, unless the
``-p no:unraisableexception`` (for unraisable exceptions) and
``-p no:threadexception`` (for thread exceptions) options are given on the
command-line.
The warnings may be silenced selectivly using the :ref:`pytest.mark.filterwarnings ref`
mark. The warning categories are :class:`pytest.PytestUnraisableExceptionWarning` and
:class:`pytest.PytestUnhandledThreadExceptionWarning`.
Creating JUnitXML format files
----------------------------------------------------

View File

@@ -265,7 +265,7 @@ Asserting warnings with the warns function
You can check that code raises a particular warning using ``pytest.warns``,
You can check that code raises a particular warning using func:`pytest.warns`,
which works in a similar manner to :ref:`raises <assertraises>`:
.. code-block:: python
@@ -293,7 +293,7 @@ argument ``match`` to assert that the exception matches a text or regex::
...
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
You can also call ``pytest.warns`` on a function or code string:
You can also call func:`pytest.warns` on a function or code string:
.. code-block:: python
@@ -328,10 +328,10 @@ Alternatively, you can examine raised warnings in detail using the
Recording warnings
------------------
You can record raised warnings either using ``pytest.warns`` or with
You can record raised warnings either using func:`pytest.warns` or with
the ``recwarn`` fixture.
To record with ``pytest.warns`` without asserting anything about the warnings,
To record with func:`pytest.warns` without asserting anything about the warnings,
pass ``None`` as the expected warning type:
.. code-block:: python
@@ -360,7 +360,7 @@ The ``recwarn`` fixture will record warnings for the whole function:
assert w.filename
assert w.lineno
Both ``recwarn`` and ``pytest.warns`` return the same interface for recorded
Both ``recwarn`` and func:`pytest.warns` return the same interface for recorded
warnings: a WarningsRecorder instance. To view the recorded warnings, you can
iterate over this instance, call ``len`` on it to get the number of recorded
warnings, or index into it to get a particular recorded warning.
@@ -387,7 +387,7 @@ are met.
pytest.fail("Expected a warning!")
If no warnings are issued when calling ``f``, then ``not record`` will
evaluate to ``True``. You can then call ``pytest.fail`` with a
evaluate to ``True``. You can then call :func:`pytest.fail` with a
custom error message.
.. _internal-warnings:

View File

@@ -33,26 +33,34 @@ Plugin discovery order at tool startup
``pytest`` loads plugin modules at tool startup in the following way:
* by loading all builtin plugins
1. by scanning the command line for the ``-p no:name`` option
and *blocking* that plugin from being loaded (even builtin plugins can
be blocked this way). This happens before normal command-line parsing.
* by loading all plugins registered through `setuptools entry points`_.
2. by loading all builtin plugins.
* by pre-scanning the command line for the ``-p name`` option
and loading the specified plugin before actual command line parsing.
3. by scanning the command line for the ``-p name`` option
and loading the specified plugin. This happens before normal command-line parsing.
* by loading all :file:`conftest.py` files as inferred by the command line
invocation:
4. by loading all plugins registered through `setuptools entry points`_.
- if no test paths are specified use current dir as a test path
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
to the directory part of the first test path.
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
Note that pytest does not find ``conftest.py`` files in deeper nested
sub directories at tool startup. It is usually a good idea to keep
your ``conftest.py`` file in the top level test or project root directory.
6. by loading all :file:`conftest.py` files as inferred by the command line
invocation:
* by recursively loading all plugins specified by the
:globalvar:`pytest_plugins` variable in ``conftest.py`` files
- if no test paths are specified, use the current dir as a test path
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
to the directory part of the first test path. After the ``conftest.py``
file is loaded, load all plugins specified in its
:globalvar:`pytest_plugins` variable if present.
Note that pytest does not find ``conftest.py`` files in deeper nested
sub directories at tool startup. It is usually a good idea to keep
your ``conftest.py`` file in the top level test or project root directory.
7. by recursively loading all plugins specified by the
:globalvar:`pytest_plugins` variable in ``conftest.py`` files.
.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/
@@ -99,6 +107,10 @@ Here is how you might run it::
See also: :ref:`pythonpath`.
.. note::
Some hooks should be implemented only in plugins or conftest.py files situated at the
tests root directory due to how pytest discovers plugins during startup,
see the documentation of each hook for details.
Writing your own plugin
-----------------------
@@ -437,13 +449,7 @@ Additionally it is possible to copy examples for an example folder before runnin
test_example.py .. [100%]
============================= warnings summary =============================
test_example.py::test_plugin
$REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time
testdir.copy_example("test_example.py")
-- Docs: https://docs.pytest.org/en/stable/warnings.html
======================= 2 passed, 1 warning in 0.12s =======================
============================ 2 passed in 0.12s =============================
For more information about the result object that ``runpytest()`` returns, and
the methods that it provides please check out the :py:class:`RunResult

View File

@@ -23,13 +23,14 @@ xfail_strict = true
filterwarnings = [
"error",
"default:Using or importing the ABCs:DeprecationWarning:unittest2.*",
# produced by older pyparsing<=2.2.0.
"default:Using or importing the ABCs:DeprecationWarning:pyparsing.*",
"default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.*",
"ignore:Module already imported so cannot be rewritten:pytest.PytestWarning",
# produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8)."
"ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest))",
# produced by pytest-xdist
"ignore:.*type argument to addoption.*:DeprecationWarning",
# produced by python >=3.5 on execnet (pytest-xdist)
# produced on execnet (pytest-xdist)
"ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning",
# pytest's own futurewarnings
"ignore::pytest.PytestExperimentalApiWarning",
@@ -102,4 +103,4 @@ template = "changelog/_template.rst"
showcontent = true
[tool.black]
target-version = ['py35']
target-version = ['py36']

View File

@@ -57,6 +57,8 @@ Created automatically from {comment_url}.
Once all builds pass and it has been **approved** by one or more maintainers, the build
can be released by pushing a tag `{version}` to this repository.
Closes #{issue_number}.
"""
@@ -164,7 +166,9 @@ def trigger_release(payload_path: Path, token: str) -> None:
print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} pushed.")
body = PR_BODY.format(
comment_url=get_comment_data(payload)["html_url"], version=version
comment_url=get_comment_data(payload)["html_url"],
version=version,
issue_number=issue_number,
)
pr = repo.create_pull(
f"Prepare release {version}",
@@ -227,7 +231,7 @@ def find_next_version(base_branch: str, is_major: bool) -> str:
msg = dedent(
f"""
Found features or breaking changes in `{base_branch}`, and feature releases can only be
created from `master`.":
created from `master`:
"""
)
msg += "\n".join(f"* `{x.name}`" for x in sorted(features + breaking))

View File

@@ -17,9 +17,7 @@ def announce(version):
stdout = stdout.decode("utf-8")
last_version = stdout.strip()
stdout = check_output(
["git", "log", "{}..HEAD".format(last_version), "--format=%aN"]
)
stdout = check_output(["git", "log", f"{last_version}..HEAD", "--format=%aN"])
stdout = stdout.decode("utf-8")
contributors = set(stdout.splitlines())
@@ -31,14 +29,10 @@ def announce(version):
Path(__file__).parent.joinpath(template_name).read_text(encoding="UTF-8")
)
contributors_text = (
"\n".join("* {}".format(name) for name in sorted(contributors)) + "\n"
)
contributors_text = "\n".join(f"* {name}" for name in sorted(contributors)) + "\n"
text = template_text.format(version=version, contributors=contributors_text)
target = Path(__file__).parent.joinpath(
"../doc/en/announce/release-{}.rst".format(version)
)
target = Path(__file__).parent.joinpath(f"../doc/en/announce/release-{version}.rst")
target.write_text(text, encoding="UTF-8")
print(f"{Fore.CYAN}[generate.announce] {Fore.RESET}Generated {target.name}")
@@ -47,7 +41,7 @@ def announce(version):
lines = index_path.read_text(encoding="UTF-8").splitlines()
indent = " "
for index, line in enumerate(lines):
if line.startswith("{}release-".format(indent)):
if line.startswith(f"{indent}release-"):
new_line = indent + target.stem
if line != new_line:
lines.insert(index, new_line)
@@ -96,7 +90,7 @@ def pre_release(version, *, skip_check_links):
if not skip_check_links:
check_links()
msg = "Prepare release version {}".format(version)
msg = f"Prepare release version {version}"
check_call(["git", "commit", "-a", "-m", msg])
print()

View File

@@ -17,7 +17,6 @@ classifiers =
Operating System :: POSIX
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
@@ -27,6 +26,8 @@ classifiers =
Topic :: Utilities
keywords = test, unittest
project_urls =
Changelog=https://docs.pytest.org/en/stable/changelog.html
Twitter=https://twitter.com/pytestdotorg
Source=https://github.com/pytest-dev/pytest
Tracker=https://github.com/pytest-dev/pytest/issues
@@ -40,22 +41,21 @@ packages =
_pytest.mark
pytest
install_requires =
attrs>=17.4.0
attrs>=19.2.0
iniconfig
packaging
pluggy>=0.12,<1.0
pluggy>=0.12,<1.0.0a1
py>=1.8.2
toml
atomicwrites>=1.0;sys_platform=="win32"
colorama;sys_platform=="win32"
importlib-metadata>=0.12;python_version<"3.8"
pathlib2>=2.2.0;python_version<"3.6"
python_requires = >=3.5
python_requires = >=3.6
package_dir =
=src
setup_requires =
setuptools>=40.0
setuptools-scm
setuptools>=>=42.0
setuptools-scm>=3.4
zip_safe = no
[options.entry_points]
@@ -64,8 +64,6 @@ console_scripts =
py.test=pytest:console_main
[options.extras_require]
checkqa-mypy =
mypy==0.780
testing =
argcomplete
hypothesis>=3.56

View File

@@ -26,7 +26,7 @@ The generic argcomplete script for bash-completion
uses a python program to determine startup script generated by pip.
You can speed up completion somewhat by changing this script to include
# PYTHON_ARGCOMPLETE_OK
so the the python-argcomplete-check-easy-install-script does not
so the python-argcomplete-check-easy-install-script does not
need to be called to find the entry point of the code and see if that is
marked with PYTHON_ARGCOMPLETE_OK.
@@ -103,7 +103,7 @@ if os.environ.get("_ARGCOMPLETE"):
import argcomplete.completers
except ImportError:
sys.exit(-1)
filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter]
filescompleter: Optional[FastFilesCompleter] = FastFilesCompleter()
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
argcomplete.autocomplete(parser, always_complete_options=False)

View File

@@ -5,6 +5,7 @@ import traceback
from inspect import CO_VARARGS
from inspect import CO_VARKEYWORDS
from io import StringIO
from pathlib import Path
from traceback import format_exception_only
from types import CodeType
from types import FrameType
@@ -17,10 +18,13 @@ from typing import Iterable
from typing import List
from typing import Mapping
from typing import Optional
from typing import overload
from typing import Pattern
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
from weakref import ref
@@ -37,15 +41,10 @@ from _pytest._code.source import Source
from _pytest._io import TerminalWriter
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr
from _pytest.compat import ATTRS_EQ_FIELD
from _pytest.compat import final
from _pytest.compat import get_real_func
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
from _pytest.pathlib import Path
if TYPE_CHECKING:
from typing import Type
from typing_extensions import Literal
from weakref import ReferenceType
@@ -55,15 +54,14 @@ if TYPE_CHECKING:
class Code:
"""Wrapper around Python code objects."""
def __init__(self, rawcode) -> None:
if not hasattr(rawcode, "co_filename"):
rawcode = getrawcode(rawcode)
if not isinstance(rawcode, CodeType):
raise TypeError("not a code object: {!r}".format(rawcode))
self.filename = rawcode.co_filename
self.firstlineno = rawcode.co_firstlineno - 1
self.name = rawcode.co_name
self.raw = rawcode
__slots__ = ("raw",)
def __init__(self, obj: CodeType) -> None:
self.raw = obj
@classmethod
def from_function(cls, obj: object) -> "Code":
return cls(getrawcode(obj))
def __eq__(self, other):
return self.raw == other.raw
@@ -71,6 +69,14 @@ class Code:
# Ignore type because of https://github.com/python/mypy/issues/4266.
__hash__ = None # type: ignore
@property
def firstlineno(self) -> int:
return self.raw.co_firstlineno - 1
@property
def name(self) -> str:
return self.raw.co_name
@property
def path(self) -> Union[py.path.local, str]:
"""Return a path object pointing to source code, or an ``str`` in
@@ -118,12 +124,26 @@ class Frame:
"""Wrapper around a Python frame holding f_locals and f_globals
in which expressions can be evaluated."""
__slots__ = ("raw",)
def __init__(self, frame: FrameType) -> None:
self.lineno = frame.f_lineno - 1
self.f_globals = frame.f_globals
self.f_locals = frame.f_locals
self.raw = frame
self.code = Code(frame.f_code)
@property
def lineno(self) -> int:
return self.raw.f_lineno - 1
@property
def f_globals(self) -> Dict[str, Any]:
return self.raw.f_globals
@property
def f_locals(self) -> Dict[str, Any]:
return self.raw.f_locals
@property
def code(self) -> Code:
return Code(self.raw.f_code)
@property
def statement(self) -> "Source":
@@ -165,17 +185,20 @@ class Frame:
class TracebackEntry:
"""A single entry in a Traceback."""
_repr_style = None # type: Optional[Literal["short", "long"]]
exprinfo = None
__slots__ = ("_rawentry", "_excinfo", "_repr_style")
def __init__(
self,
rawentry: TracebackType,
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
) -> None:
self._excinfo = excinfo
self._rawentry = rawentry
self.lineno = rawentry.tb_lineno - 1
self._excinfo = excinfo
self._repr_style: Optional['Literal["short", "long"]'] = None
@property
def lineno(self) -> int:
return self._rawentry.tb_lineno - 1
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
assert mode in ("short", "long")
@@ -247,9 +270,9 @@ class TracebackEntry:
Mostly for internal use.
"""
tbh = (
tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = (
False
) # type: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]]
)
for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
# in normal cases, f_locals and f_globals are dictionaries
# however via `exec(...)` / `eval(...)` they can be other types
@@ -302,7 +325,7 @@ class Traceback(List[TracebackEntry]):
if isinstance(tb, TracebackType):
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
cur_ = cur # type: Optional[TracebackType]
cur_: Optional[TracebackType] = cur
while cur_ is not None:
yield TracebackEntry(cur_, excinfo=excinfo)
cur_ = cur_.tb_next
@@ -347,13 +370,11 @@ class Traceback(List[TracebackEntry]):
def __getitem__(self, key: int) -> TracebackEntry:
...
@overload # noqa: F811
def __getitem__(self, key: slice) -> "Traceback": # noqa: F811
@overload
def __getitem__(self, key: slice) -> "Traceback":
...
def __getitem__( # noqa: F811
self, key: Union[int, slice]
) -> Union[TracebackEntry, "Traceback"]:
def __getitem__(self, key: Union[int, slice]) -> Union[TracebackEntry, "Traceback"]:
if isinstance(key, slice):
return self.__class__(super().__getitem__(key))
else:
@@ -384,7 +405,7 @@ class Traceback(List[TracebackEntry]):
def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if
appropriate, None if no recursion occurred."""
cache = {} # type: Dict[Tuple[Any, int, int], List[Dict[str, Any]]]
cache: 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
@@ -422,14 +443,14 @@ class ExceptionInfo(Generic[_E]):
_assert_start_repr = "AssertionError('assert "
_excinfo = attr.ib(type=Optional[Tuple["Type[_E]", "_E", TracebackType]])
_excinfo = attr.ib(type=Optional[Tuple[Type["_E"], "_E", TracebackType]])
_striptext = attr.ib(type=str, default="")
_traceback = attr.ib(type=Optional[Traceback], default=None)
@classmethod
def from_exc_info(
cls,
exc_info: Tuple["Type[_E]", "_E", TracebackType],
exc_info: Tuple[Type[_E], _E, TracebackType],
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[_E]":
"""Return an ExceptionInfo for an existing exc_info tuple.
@@ -480,13 +501,13 @@ class ExceptionInfo(Generic[_E]):
"""Return an unfilled ExceptionInfo."""
return cls(None)
def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None:
def fill_unfilled(self, exc_info: Tuple[Type[_E], _E, TracebackType]) -> None:
"""Fill an unfilled ExceptionInfo created with ``for_later()``."""
assert self._excinfo is None, "ExceptionInfo was already filled"
self._excinfo = exc_info
@property
def type(self) -> "Type[_E]":
def type(self) -> Type[_E]:
"""The exception class."""
assert (
self._excinfo is not None
@@ -552,7 +573,7 @@ class ExceptionInfo(Generic[_E]):
return text
def errisinstance(
self, exc: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]]
self, exc: Union[Type[BaseException], Tuple[Type[BaseException], ...]]
) -> bool:
"""Return True if the exception is an instance of exc.
@@ -626,7 +647,7 @@ class ExceptionInfo(Generic[_E]):
)
return fmt.repr_excinfo(self)
def match(self, regexp: "Union[str, Pattern[str]]") -> "Literal[True]":
def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
"""Check whether the regular expression `regexp` matches the string
representation of the exception using :func:`python:re.search`.
@@ -750,7 +771,7 @@ class FormattedExcinfo:
else:
str_repr = safeformat(value)
# if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
lines.append("{:<10} = {}".format(name, str_repr))
lines.append(f"{name:<10} = {str_repr}")
# else:
# self._line("%-10s =\\" % (name,))
# # XXX
@@ -763,7 +784,7 @@ class FormattedExcinfo:
entry: TracebackEntry,
excinfo: Optional[ExceptionInfo[BaseException]] = None,
) -> "ReprEntry":
lines = [] # type: List[str]
lines: List[str] = []
style = entry._repr_style if entry._repr_style is not None else self.style
if style in ("short", "long"):
source = self._getentrysource(entry)
@@ -845,7 +866,7 @@ class FormattedExcinfo:
recursionindex = traceback.recursionindex()
except Exception as e:
max_frames = 10
extraline = (
extraline: Optional[str] = (
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
" The following exception happened when comparing locals in the stack frame:\n"
" {exc_type}: {exc_msg}\n"
@@ -855,7 +876,7 @@ class FormattedExcinfo:
exc_msg=str(e),
max_frames=max_frames,
total=len(traceback),
) # type: Optional[str]
)
# Type ignored because adding two instaces of a List subtype
# currently incorrectly has type List instead of the subtype.
traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
@@ -871,20 +892,20 @@ class FormattedExcinfo:
def repr_excinfo(
self, excinfo: ExceptionInfo[BaseException]
) -> "ExceptionChainRepr":
repr_chain = (
[]
) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]]
e = excinfo.value # type: Optional[BaseException]
excinfo_ = excinfo # type: Optional[ExceptionInfo[BaseException]]
repr_chain: List[
Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]
] = []
e: Optional[BaseException] = excinfo.value
excinfo_: Optional[ExceptionInfo[BaseException]] = excinfo
descr = None
seen = set() # type: Set[int]
seen: Set[int] = set()
while e is not None and id(e) not in seen:
seen.add(id(e))
if excinfo_:
reprtraceback = self.repr_traceback(excinfo_)
reprcrash = (
reprcrash: Optional[ReprFileLocation] = (
excinfo_._getreprcrash() if self.style != "value" else None
) # type: Optional[ReprFileLocation]
)
else:
# Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work.
@@ -918,7 +939,7 @@ class FormattedExcinfo:
return ExceptionChainRepr(repr_chain)
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
@attr.s(eq=False)
class TerminalRepr:
def __str__(self) -> str:
# FYI this is called from pytest-xdist's serialization of exception
@@ -936,14 +957,14 @@ class TerminalRepr:
# This class is abstract -- only subclasses are instantiated.
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
@attr.s(eq=False)
class ExceptionRepr(TerminalRepr):
# Provided by subclasses.
reprcrash = None # type: Optional[ReprFileLocation]
reprtraceback = None # type: ReprTraceback
reprcrash: Optional["ReprFileLocation"]
reprtraceback: "ReprTraceback"
def __attrs_post_init__(self) -> None:
self.sections = [] # type: List[Tuple[str, str, str]]
self.sections: List[Tuple[str, str, str]] = []
def addsection(self, name: str, content: str, sep: str = "-") -> None:
self.sections.append((name, content, sep))
@@ -954,7 +975,7 @@ class ExceptionRepr(TerminalRepr):
tw.line(content)
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
@attr.s(eq=False)
class ExceptionChainRepr(ExceptionRepr):
chain = attr.ib(
type=Sequence[
@@ -978,7 +999,7 @@ class ExceptionChainRepr(ExceptionRepr):
super().toterminal(tw)
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
@attr.s(eq=False)
class ReprExceptionInfo(ExceptionRepr):
reprtraceback = attr.ib(type="ReprTraceback")
reprcrash = attr.ib(type="ReprFileLocation")
@@ -988,7 +1009,7 @@ class ReprExceptionInfo(ExceptionRepr):
super().toterminal(tw)
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
@attr.s(eq=False)
class ReprTraceback(TerminalRepr):
reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]])
extraline = attr.ib(type=Optional[str])
@@ -1022,16 +1043,16 @@ class ReprTracebackNative(ReprTraceback):
self.extraline = None
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
@attr.s(eq=False)
class ReprEntryNative(TerminalRepr):
lines = attr.ib(type=Sequence[str])
style = "native" # type: _TracebackStyle
style: "_TracebackStyle" = "native"
def toterminal(self, tw: TerminalWriter) -> None:
tw.write("".join(self.lines))
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
@attr.s(eq=False)
class ReprEntry(TerminalRepr):
lines = attr.ib(type=Sequence[str])
reprfuncargs = attr.ib(type=Optional["ReprFuncArgs"])
@@ -1059,11 +1080,11 @@ class ReprEntry(TerminalRepr):
# separate indents and source lines that are not failures: we want to
# highlight the code but not the indentation, which may contain markers
# such as "> assert 0"
fail_marker = "{} ".format(FormattedExcinfo.fail_marker)
fail_marker = f"{FormattedExcinfo.fail_marker} "
indent_size = len(fail_marker)
indents = [] # type: List[str]
source_lines = [] # type: List[str]
failure_lines = [] # type: List[str]
indents: List[str] = []
source_lines: List[str] = []
failure_lines: List[str] = []
for index, line in enumerate(self.lines):
is_failure_line = line.startswith(fail_marker)
if is_failure_line:
@@ -1111,7 +1132,7 @@ class ReprEntry(TerminalRepr):
)
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
@attr.s(eq=False)
class ReprFileLocation(TerminalRepr):
path = attr.ib(type=str, converter=str)
lineno = attr.ib(type=int)
@@ -1125,10 +1146,10 @@ class ReprFileLocation(TerminalRepr):
if i != -1:
msg = msg[:i]
tw.write(self.path, bold=True, red=True)
tw.line(":{}: {}".format(self.lineno, msg))
tw.line(f":{self.lineno}: {msg}")
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
@attr.s(eq=False)
class ReprLocals(TerminalRepr):
lines = attr.ib(type=Sequence[str])
@@ -1137,7 +1158,7 @@ class ReprLocals(TerminalRepr):
tw.line(indent + line)
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
@attr.s(eq=False)
class ReprFuncArgs(TerminalRepr):
args = attr.ib(type=Sequence[Tuple[str, object]])
@@ -1145,7 +1166,7 @@ class ReprFuncArgs(TerminalRepr):
if self.args:
linesofar = ""
for name, value in self.args:
ns = "{} = {}".format(name, value)
ns = f"{name} = {value}"
if len(ns) + len(linesofar) + 2 > tw.fullwidth:
if linesofar:
tw.line(linesofar)
@@ -1175,7 +1196,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]:
obj = obj.place_as # type: ignore[attr-defined]
try:
code = Code(obj)
code = Code.from_function(obj)
except TypeError:
try:
fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]

View File

@@ -2,17 +2,17 @@ import ast
import inspect
import textwrap
import tokenize
import types
import warnings
from bisect import bisect_right
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import overload
from typing import Tuple
from typing import Union
from _pytest.compat import overload
class Source:
"""An immutable object holding a source code fragment.
@@ -22,7 +22,7 @@ class Source:
def __init__(self, obj: object = None) -> None:
if not obj:
self.lines = [] # type: List[str]
self.lines: List[str] = []
elif isinstance(obj, Source):
self.lines = obj.lines
elif isinstance(obj, (tuple, list)):
@@ -30,8 +30,11 @@ class Source:
elif isinstance(obj, str):
self.lines = deindent(obj.split("\n"))
else:
rawcode = getrawcode(obj)
src = inspect.getsource(rawcode)
try:
rawcode = getrawcode(obj)
src = inspect.getsource(rawcode)
except TypeError:
src = inspect.getsource(obj) # type: ignore[arg-type]
self.lines = deindent(src.split("\n"))
def __eq__(self, other: object) -> bool:
@@ -46,11 +49,11 @@ class Source:
def __getitem__(self, key: int) -> str:
...
@overload # noqa: F811
def __getitem__(self, key: slice) -> "Source": # noqa: F811
@overload
def __getitem__(self, key: slice) -> "Source":
...
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:
if isinstance(key, int):
return self.lines[key]
else:
@@ -123,19 +126,17 @@ def findsource(obj) -> Tuple[Optional[Source], int]:
return source, lineno
def getrawcode(obj, trycall: bool = True):
def getrawcode(obj: object, trycall: bool = True) -> types.CodeType:
"""Return code object for given function."""
try:
return obj.__code__
return obj.__code__ # type: ignore[attr-defined,no-any-return]
except AttributeError:
obj = getattr(obj, "f_code", obj)
obj = getattr(obj, "__code__", obj)
if trycall and not hasattr(obj, "co_firstlineno"):
if hasattr(obj, "__call__") and not inspect.isclass(obj):
x = getrawcode(obj.__call__, trycall=False)
if hasattr(x, "co_firstlineno"):
return x
return obj
pass
if trycall:
call = getattr(obj, "__call__", None)
if call and not isinstance(obj, type):
return getrawcode(call, trycall=False)
raise TypeError(f"could not get code object for {obj!r}")
def deindent(lines: Iterable[str]) -> List[str]:
@@ -145,12 +146,12 @@ def deindent(lines: Iterable[str]) -> List[str]:
def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
# Flatten all statements and except handlers into one lineno-list.
# AST's line numbers start indexing at 1.
values = [] # type: List[int]
values: List[int] = []
for x in ast.walk(node):
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
values.append(x.lineno - 1)
for name in ("finalbody", "orelse"):
val = getattr(x, name, None) # type: Optional[List[ast.stmt]]
val: Optional[List[ast.stmt]] = getattr(x, name, None)
if val:
# Treat the finally/orelse part as its own statement.
values.append(val[0].lineno - 1 - 1)

View File

@@ -122,7 +122,7 @@ def _pformat_dispatch(
width: int = 80,
depth: Optional[int] = None,
*,
compact: bool = False
compact: bool = False,
) -> str:
return AlwaysDispatchingPrettyPrinter(
indent=indent, width=width, depth=depth, compact=compact

View File

@@ -76,7 +76,7 @@ class TerminalWriter:
self._file = file
self.hasmarkup = should_do_markup(file)
self._current_line = ""
self._terminal_width = None # type: Optional[int]
self._terminal_width: Optional[int] = None
self.code_highlight = True
@property
@@ -97,7 +97,7 @@ class TerminalWriter:
def markup(self, text: str, **markup: bool) -> str:
for name in markup:
if name not in self._esctable:
raise ValueError("unknown markup: {!r}".format(name))
raise ValueError(f"unknown markup: {name!r}")
if self.hasmarkup:
esc = [self._esctable[name] for name, on in markup.items() if on]
if esc:
@@ -109,7 +109,7 @@ class TerminalWriter:
sepchar: str,
title: Optional[str] = None,
fullwidth: Optional[int] = None,
**markup: bool
**markup: bool,
) -> None:
if fullwidth is None:
fullwidth = self.fullwidth
@@ -128,7 +128,7 @@ class TerminalWriter:
# N <= (fullwidth - len(title) - 2) // (2*len(sepchar))
N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1)
fill = sepchar * N
line = "{} {} {}".format(fill, title, fill)
line = f"{fill} {title} {fill}"
else:
# we want len(sepchar)*N <= fullwidth
# i.e. N <= fullwidth // len(sepchar)
@@ -204,7 +204,7 @@ class TerminalWriter:
except ImportError:
return source
else:
highlighted = highlight(
highlighted: str = highlight(
source, PythonLexer(), TerminalFormatter(bg="dark")
) # type: str
)
return highlighted

View File

@@ -4,12 +4,12 @@ from typing import Any
from typing import Generator
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from _pytest.assertion import rewrite
from _pytest.assertion import truncate
from _pytest.assertion import util
from _pytest.assertion.rewrite import assertstate_key
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
@@ -83,7 +83,7 @@ class AssertionState:
def __init__(self, config: Config, mode) -> None:
self.mode = mode
self.trace = config.trace.root.get("assertion")
self.hook = None # type: Optional[rewrite.AssertionRewritingHook]
self.hook: Optional[rewrite.AssertionRewritingHook] = None
def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:

View File

@@ -13,6 +13,8 @@ import struct
import sys
import tokenize
import types
from pathlib import Path
from pathlib import PurePath
from typing import Callable
from typing import Dict
from typing import IO
@@ -22,6 +24,7 @@ from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import py
@@ -32,24 +35,20 @@ from _pytest.assertion import util
from _pytest.assertion.util import ( # noqa: F401
format_explanation as _format_explanation,
)
from _pytest.compat import fspath
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.main import Session
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import Path
from _pytest.pathlib import PurePath
from _pytest.store import StoreKey
if TYPE_CHECKING:
from _pytest.assertion import AssertionState # noqa: F401
from _pytest.assertion import AssertionState
assertstate_key = StoreKey["AssertionState"]()
# pytest caches rewritten pycs in pycache dirs
PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version)
PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
PYC_EXT = ".py" + (__debug__ and "c" or "o")
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
@@ -63,14 +62,14 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
self.fnpats = config.getini("python_files")
except ValueError:
self.fnpats = ["test_*.py", "*_test.py"]
self.session = None # type: Optional[Session]
self._rewritten_names = set() # type: Set[str]
self._must_rewrite = set() # type: Set[str]
self.session: Optional[Session] = None
self._rewritten_names: Set[str] = set()
self._must_rewrite: Set[str] = set()
# 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 = {} # type: Dict[str, bool]
self._marked_for_rewrite_cache: Dict[str, bool] = {}
self._session_paths_checked = False
def set_session(self, session: Optional[Session]) -> None:
@@ -100,7 +99,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
spec is None
# this is a namespace package (without `__init__.py`)
# there's nothing to rewrite there
# python3.5 - python3.6: `namespace`
# python3.6: `namespace`
# python3.7+: `None`
or spec.origin == "namespace"
or spec.origin is None
@@ -150,7 +149,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
ok = try_makedirs(cache_dir)
if not ok:
write = False
state.trace("read only directory: {}".format(cache_dir))
state.trace(f"read only directory: {cache_dir}")
cache_name = fn.name[:-3] + PYC_TAIL
pyc = cache_dir / cache_name
@@ -158,7 +157,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
# to check for a cached pyc. This may not be optimal...
co = _read_pyc(fn, pyc, state.trace)
if co is None:
state.trace("rewriting {!r}".format(fn))
state.trace(f"rewriting {fn!r}")
source_stat, co = _rewrite_test(fn, self.config)
if write:
self._writing_pyc = True
@@ -167,7 +166,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
finally:
self._writing_pyc = False
else:
state.trace("found cached rewritten pyc for {}".format(fn))
state.trace(f"found cached rewritten pyc for {fn}")
exec(co, module.__dict__)
def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool:
@@ -206,20 +205,18 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
if self._is_marked_for_rewrite(name, state):
return False
state.trace("early skip of rewriting module: {}".format(name))
state.trace(f"early skip of rewriting module: {name}")
return True
def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool:
# always rewrite conftest files
if os.path.basename(fn) == "conftest.py":
state.trace("rewriting conftest file: {!r}".format(fn))
state.trace(f"rewriting conftest file: {fn!r}")
return True
if self.session is not None:
if self.session.isinitpath(py.path.local(fn)):
state.trace(
"matched test file (was specified on cmdline): {!r}".format(fn)
)
state.trace(f"matched test file (was specified on cmdline): {fn!r}")
return True
# modules not passed explicitly on the command line are only
@@ -227,7 +224,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
fn_path = PurePath(fn)
for pat in self.fnpats:
if fnmatch_ex(pat, fn_path):
state.trace("matched test file {!r}".format(fn))
state.trace(f"matched test file {fn!r}")
return True
return self._is_marked_for_rewrite(name, state)
@@ -238,9 +235,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
except KeyError:
for marked in self._must_rewrite:
if name == marked or name.startswith(marked + "."):
state.trace(
"matched marked file {!r} (from {!r})".format(name, marked)
)
state.trace(f"matched marked file {name!r} (from {marked!r})")
self._marked_for_rewrite_cache[name] = True
return True
@@ -286,12 +281,16 @@ def _write_pyc_fp(
) -> None:
# Technically, we don't have to have the same pyc format as
# (C)Python, since these "pycs" should never be seen by builtin
# import. However, there's little reason deviate.
# import. However, there's little reason to deviate.
fp.write(importlib.util.MAGIC_NUMBER)
# https://www.python.org/dev/peps/pep-0552/
if sys.version_info >= (3, 7):
flags = b"\x00\x00\x00\x00"
fp.write(flags)
# as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
size = source_stat.st_size & 0xFFFFFFFF
# "<LL" stands for 2 unsigned longs, little-ending
# "<LL" stands for 2 unsigned longs, little-endian.
fp.write(struct.pack("<LL", mtime, size))
fp.write(marshal.dumps(co))
@@ -306,10 +305,10 @@ if sys.platform == "win32":
pyc: Path,
) -> bool:
try:
with atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp:
with atomic_write(os.fspath(pyc), mode="wb", overwrite=True) as fp:
_write_pyc_fp(fp, source_stat, co)
except OSError as e:
state.trace("error writing pyc file at {}: {}".format(pyc, e))
state.trace(f"error writing pyc file at {pyc}: {e}")
# we ignore any failure to write the cache file
# there are many reasons, permission-denied, pycache dir being a
# file etc.
@@ -325,20 +324,18 @@ else:
source_stat: os.stat_result,
pyc: Path,
) -> bool:
proc_pyc = "{}.{}".format(pyc, os.getpid())
proc_pyc = f"{pyc}.{os.getpid()}"
try:
fp = open(proc_pyc, "wb")
except OSError as e:
state.trace(
"error writing pyc file at {}: errno={}".format(proc_pyc, e.errno)
)
state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
return False
try:
_write_pyc_fp(fp, source_stat, co)
os.rename(proc_pyc, fspath(pyc))
os.rename(proc_pyc, os.fspath(pyc))
except OSError as e:
state.trace("error writing pyc file at {}: {}".format(pyc, e))
state.trace(f"error writing pyc file at {pyc}: {e}")
# we ignore any failure to write the cache file
# there are many reasons, permission-denied, pycache dir being a
# file etc.
@@ -350,7 +347,7 @@ else:
def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
"""Read and rewrite *fn* and return the code object."""
fn_ = fspath(fn)
fn_ = os.fspath(fn)
stat = os.stat(fn_)
with open(fn_, "rb") as f:
source = f.read()
@@ -368,30 +365,42 @@ def _read_pyc(
Return rewritten code if successful or None if not.
"""
try:
fp = open(fspath(pyc), "rb")
fp = open(os.fspath(pyc), "rb")
except OSError:
return None
with fp:
# https://www.python.org/dev/peps/pep-0552/
has_flags = sys.version_info >= (3, 7)
try:
stat_result = os.stat(fspath(source))
stat_result = os.stat(os.fspath(source))
mtime = int(stat_result.st_mtime)
size = stat_result.st_size
data = fp.read(12)
data = fp.read(16 if has_flags else 12)
except OSError as e:
trace("_read_pyc({}): OSError {}".format(source, e))
trace(f"_read_pyc({source}): OSError {e}")
return None
# Check for invalid or out of date pyc file.
if (
len(data) != 12
or data[:4] != importlib.util.MAGIC_NUMBER
or struct.unpack("<LL", data[4:]) != (mtime & 0xFFFFFFFF, size & 0xFFFFFFFF)
):
trace("_read_pyc(%s): invalid or out of date pyc" % source)
if len(data) != (16 if has_flags else 12):
trace("_read_pyc(%s): invalid pyc (too short)" % source)
return None
if data[:4] != importlib.util.MAGIC_NUMBER:
trace("_read_pyc(%s): invalid pyc (bad magic number)" % source)
return None
if has_flags and data[4:8] != b"\x00\x00\x00\x00":
trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source)
return None
mtime_data = data[8 if has_flags else 4 : 12 if has_flags else 8]
if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
trace("_read_pyc(%s): out of date" % source)
return None
size_data = data[12 if has_flags else 8 : 16 if has_flags else 12]
if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
trace("_read_pyc(%s): invalid pyc (incorrect size)" % source)
return None
try:
co = marshal.load(fp)
except Exception as e:
trace("_read_pyc({}): marshal.load error {}".format(source, e))
trace(f"_read_pyc({source}): marshal.load error {e}")
return None
if not isinstance(co, types.CodeType):
trace("_read_pyc(%s): not a code object" % source)
@@ -536,12 +545,12 @@ def set_location(node, lineno, col_offset):
def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
"""Return a mapping from {lineno: "assertion test expression"}."""
ret = {} # type: Dict[int, str]
ret: Dict[int, str] = {}
depth = 0
lines = [] # type: List[str]
assert_lineno = None # type: Optional[int]
seen_lines = set() # type: Set[int]
lines: List[str] = []
assert_lineno: Optional[int] = None
seen_lines: Set[int] = set()
def _write_and_reset() -> None:
nonlocal depth, lines, assert_lineno, seen_lines
@@ -706,12 +715,12 @@ class AssertionRewriter(ast.NodeVisitor):
]
mod.body[pos:pos] = imports
# Collect asserts.
nodes = [mod] # type: List[ast.AST]
nodes: List[ast.AST] = [mod]
while nodes:
node = nodes.pop()
for name, field in ast.iter_fields(node):
if isinstance(field, list):
new = [] # type: List[ast.AST]
new: List[ast.AST] = []
for i, child in enumerate(field):
if isinstance(child, ast.Assert):
# Transform assert.
@@ -783,7 +792,7 @@ class AssertionRewriter(ast.NodeVisitor):
to format a string of %-formatted values as added by
.explanation_param().
"""
self.explanation_specifiers = {} # type: Dict[str, ast.expr]
self.explanation_specifiers: Dict[str, ast.expr] = {}
self.stack.append(self.explanation_specifiers)
def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
@@ -831,19 +840,19 @@ class AssertionRewriter(ast.NodeVisitor):
"assertion is always true, perhaps remove parentheses?"
),
category=None,
filename=fspath(self.module_path),
filename=os.fspath(self.module_path),
lineno=assert_.lineno,
)
self.statements = [] # type: List[ast.stmt]
self.variables = [] # type: List[str]
self.statements: List[ast.stmt] = []
self.variables: List[str] = []
self.variable_counter = itertools.count()
if self.enable_assertion_pass_hook:
self.format_variables = [] # type: List[str]
self.format_variables: List[str] = []
self.stack = [] # type: List[Dict[str, ast.expr]]
self.expl_stmts = [] # type: List[ast.stmt]
self.stack: List[Dict[str, ast.expr]] = []
self.expl_stmts: List[ast.stmt] = []
self.push_format_context()
# Rewrite assert into a bunch of statements.
top_condition, explanation = self.visit(assert_.test)
@@ -950,7 +959,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Process each operand, short-circuiting if needed.
for i, v in enumerate(boolop.values):
if i:
fail_inner = [] # type: List[ast.stmt]
fail_inner: 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
@@ -961,10 +970,10 @@ class AssertionRewriter(ast.NodeVisitor):
call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call))
if i < levels:
cond = res # type: ast.expr
cond: ast.expr = res
if is_or:
cond = ast.UnaryOp(ast.Not(), cond)
inner = [] # type: List[ast.stmt]
inner: List[ast.stmt] = []
self.statements.append(ast.If(cond, inner, []))
self.statements = body = inner
self.statements = save
@@ -983,7 +992,7 @@ class AssertionRewriter(ast.NodeVisitor):
symbol = BINOP_MAP[binop.op.__class__]
left_expr, left_expl = self.visit(binop.left)
right_expr, right_expl = self.visit(binop.right)
explanation = "({} {} {})".format(left_expl, symbol, right_expl)
explanation = f"({left_expl} {symbol} {right_expl})"
res = self.assign(ast.BinOp(left_expr, binop.op, right_expr))
return res, explanation
@@ -1008,11 +1017,11 @@ class AssertionRewriter(ast.NodeVisitor):
new_call = ast.Call(new_func, new_args, new_kwargs)
res = self.assign(new_call)
res_expl = self.explanation_param(self.display(res))
outer_expl = "{}\n{{{} = {}\n}}".format(res_expl, res_expl, expl)
outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}"
return res, outer_expl
def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]:
# From Python 3.5, a Starred node can appear in a function call.
# A Starred node can appear in a function call.
res, expl = self.visit(starred.value)
new_starred = ast.Starred(res, starred.ctx)
return new_starred, "*" + expl
@@ -1031,7 +1040,7 @@ class AssertionRewriter(ast.NodeVisitor):
self.push_format_context()
left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = "({})".format(left_expl)
left_expl = f"({left_expl})"
res_variables = [self.variable() for i in range(len(comp.ops))]
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
store_names = [ast.Name(v, ast.Store()) for v in res_variables]
@@ -1042,11 +1051,11 @@ class AssertionRewriter(ast.NodeVisitor):
for i, op, next_operand in it:
next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = "({})".format(next_expl)
next_expl = f"({next_expl})"
results.append(next_res)
sym = BINOP_MAP[op.__class__]
syms.append(ast.Str(sym))
expl = "{} {} {}".format(left_expl, sym, next_expl)
expl = f"{left_expl} {sym} {next_expl}"
expls.append(ast.Str(expl))
res_expr = ast.Compare(left_res, [op], [next_res])
self.statements.append(ast.Assign([store_names[i]], res_expr))
@@ -1060,7 +1069,7 @@ class AssertionRewriter(ast.NodeVisitor):
ast.Tuple(results, ast.Load()),
)
if len(comp.ops) > 1:
res = ast.BoolOp(ast.And(), load_names) # type: ast.expr
res: ast.expr = ast.BoolOp(ast.And(), load_names)
else:
res = load_names[0]
return res, self.explanation_param(self.pop_format_context(expl_call))
@@ -1072,7 +1081,7 @@ def try_makedirs(cache_dir: Path) -> bool:
Returns True if successful or if it already exists.
"""
try:
os.makedirs(fspath(cache_dir), exist_ok=True)
os.makedirs(os.fspath(cache_dir), exist_ok=True)
except (FileNotFoundError, NotADirectoryError, FileExistsError):
# One of the path components was not a directory:
# - we're in a zip file

View File

@@ -70,10 +70,10 @@ def _truncate_explanation(
truncated_line_count += 1 # Account for the part-truncated final line
msg = "...Full output truncated"
if truncated_line_count == 1:
msg += " ({} line hidden)".format(truncated_line_count)
msg += f" ({truncated_line_count} line hidden)"
else:
msg += " ({} lines hidden)".format(truncated_line_count)
msg += ", {}".format(USAGE_MSG)
msg += f" ({truncated_line_count} lines hidden)"
msg += f", {USAGE_MSG}"
truncated_explanation.extend(["", str(msg)])
return truncated_explanation

View File

@@ -9,24 +9,22 @@ from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Tuple
import _pytest._code
from _pytest import outcomes
from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr
from _pytest.compat import ATTRS_EQ_FIELD
# The _reprcompare attribute on the util module is used by the new assertion
# 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 # type: Optional[Callable[[str, object, object], Optional[str]]]
_reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None
# Works similarly as _reprcompare attribute. Is populated with the hook call
# when pytest_runtest_setup is called.
_assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
_assertion_pass: Optional[Callable[[int, str, str], None]] = None
def format_explanation(explanation: str) -> str:
@@ -112,6 +110,10 @@ def isset(x: Any) -> bool:
return isinstance(x, (set, frozenset))
def isnamedtuple(obj: Any) -> bool:
return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
def isdatacls(obj: Any) -> bool:
return getattr(obj, "__dataclass_fields__", None) is not None
@@ -143,7 +145,7 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[
left_repr = saferepr(left, maxsize=maxsize)
right_repr = saferepr(right, maxsize=maxsize)
summary = "{} {} {}".format(left_repr, op, right_repr)
summary = f"{left_repr} {op} {right_repr}"
explanation = None
try:
@@ -173,15 +175,20 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
if istext(left) and istext(right):
explanation = _diff_text(left, right, verbose)
else:
if issequence(left) and issequence(right):
if type(left) == type(right) and (
isdatacls(left) or isattrs(left) or isnamedtuple(left)
):
# Note: unlike dataclasses/attrs, namedtuples compare only the
# field values, not the type or field names. But this branch
# intentionally only handles the same-type case, which was often
# used in older code bases before dataclasses/attrs were available.
explanation = _compare_eq_cls(left, right, verbose)
elif issequence(left) and issequence(right):
explanation = _compare_eq_sequence(left, right, verbose)
elif isset(left) and isset(right):
explanation = _compare_eq_set(left, right, verbose)
elif isdict(left) and isdict(right):
explanation = _compare_eq_dict(left, right, verbose)
elif type(left) == type(right) and (isdatacls(left) or isattrs(left)):
type_fn = (isdatacls, isattrs)
explanation = _compare_eq_cls(left, right, verbose, type_fn)
elif verbose > 0:
explanation = _compare_eq_verbose(left, right)
if isiterable(left) and isiterable(right):
@@ -198,7 +205,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
"""
from difflib import ndiff
explanation = [] # type: List[str]
explanation: List[str] = []
if verbose < 1:
i = 0 # just in case left or right has zero length
@@ -243,7 +250,7 @@ def _compare_eq_verbose(left: Any, right: Any) -> List[str]:
left_lines = repr(left).splitlines(keepends)
right_lines = repr(right).splitlines(keepends)
explanation = [] # type: List[str]
explanation: List[str] = []
explanation += ["+" + line for line in left_lines]
explanation += ["-" + line for line in right_lines]
@@ -297,7 +304,7 @@ def _compare_eq_sequence(
left: Sequence[Any], right: Sequence[Any], verbose: int = 0
) -> List[str]:
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
explanation = [] # type: List[str]
explanation: List[str] = []
len_left = len(left)
len_right = len(right)
for i in range(min(len_left, len_right)):
@@ -317,9 +324,7 @@ def _compare_eq_sequence(
left_value = left[i]
right_value = right[i]
explanation += [
"At index {} diff: {!r} != {!r}".format(i, left_value, right_value)
]
explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"]
break
if comparing_bytes:
@@ -339,9 +344,7 @@ def _compare_eq_sequence(
extra = saferepr(right[len_left])
if len_diff == 1:
explanation += [
"{} contains one more item: {}".format(dir_with_more, extra)
]
explanation += [f"{dir_with_more} contains one more item: {extra}"]
else:
explanation += [
"%s contains %d more items, first extra item: %s"
@@ -370,7 +373,7 @@ def _compare_eq_set(
def _compare_eq_dict(
left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0
) -> List[str]:
explanation = [] # type: List[str]
explanation: List[str] = []
set_left = set(left)
set_right = set(right)
common = set_left.intersection(set_right)
@@ -408,21 +411,17 @@ def _compare_eq_dict(
return explanation
def _compare_eq_cls(
left: Any,
right: Any,
verbose: int,
type_fns: Tuple[Callable[[Any], bool], Callable[[Any], bool]],
) -> List[str]:
isdatacls, isattrs = type_fns
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
if isdatacls(left):
all_fields = left.__dataclass_fields__
fields_to_check = [field for field, info in all_fields.items() if info.compare]
elif isattrs(left):
all_fields = left.__attrs_attrs__
fields_to_check = [
field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD)
]
fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
elif isnamedtuple(left):
fields_to_check = left._fields
else:
assert False
indent = " "
same = []

View File

@@ -3,6 +3,7 @@
# pytest-cache version.
import json
import os
from pathlib import Path
from typing import Dict
from typing import Generator
from typing import Iterable
@@ -14,21 +15,22 @@ from typing import Union
import attr
import py
import pytest
from .pathlib import Path
from .pathlib import resolve_from_str
from .pathlib import rm_rf
from .reports import CollectReport
from _pytest import nodes
from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.compat import order_preserving_dict
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.main import Session
from _pytest.python import Module
from _pytest.python import Package
from _pytest.reports import TestReport
@@ -52,7 +54,7 @@ Signature: 8a477f597d28d172789f06886806bc55
@final
@attr.s
@attr.s(init=False)
class Cache:
_cachedir = attr.ib(type=Path, repr=False)
_config = attr.ib(type=Config, repr=False)
@@ -63,26 +65,52 @@ class Cache:
# sub-directory under cache-dir for values created by "set"
_CACHE_PREFIX_VALUES = "v"
@classmethod
def for_config(cls, config: Config) -> "Cache":
cachedir = cls.cache_dir_from_config(config)
if config.getoption("cacheclear") and cachedir.is_dir():
cls.clear_cache(cachedir)
return cls(cachedir, config)
def __init__(
self, cachedir: Path, config: Config, *, _ispytest: bool = False
) -> None:
check_ispytest(_ispytest)
self._cachedir = cachedir
self._config = config
@classmethod
def clear_cache(cls, cachedir: Path) -> None:
"""Clear the sub-directories used to hold cached directories and values."""
def for_config(cls, config: Config, *, _ispytest: bool = False) -> "Cache":
"""Create the Cache instance for a Config.
:meta private:
"""
check_ispytest(_ispytest)
cachedir = cls.cache_dir_from_config(config, _ispytest=True)
if config.getoption("cacheclear") and cachedir.is_dir():
cls.clear_cache(cachedir, _ispytest=True)
return cls(cachedir, config, _ispytest=True)
@classmethod
def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None:
"""Clear the sub-directories used to hold cached directories and values.
:meta private:
"""
check_ispytest(_ispytest)
for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
d = cachedir / prefix
if d.is_dir():
rm_rf(d)
@staticmethod
def cache_dir_from_config(config: Config) -> Path:
def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path:
"""Get the path to the cache directory for a Config.
:meta private:
"""
check_ispytest(_ispytest)
return resolve_from_str(config.getini("cache_dir"), config.rootpath)
def warn(self, fmt: str, **args: object) -> None:
def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
"""Issue a cache warning.
:meta private:
"""
check_ispytest(_ispytest)
import warnings
from _pytest.warning_types import PytestCacheWarning
@@ -151,7 +179,7 @@ class Cache:
cache_dir_exists_already = self._cachedir.exists()
path.parent.mkdir(exist_ok=True, parents=True)
except OSError:
self.warn("could not create cache path {path}", path=path)
self.warn("could not create cache path {path}", path=path, _ispytest=True)
return
if not cache_dir_exists_already:
self._ensure_supporting_files()
@@ -159,7 +187,7 @@ class Cache:
try:
f = path.open("w")
except OSError:
self.warn("cache could not write path {path}", path=path)
self.warn("cache could not write path {path}", path=path, _ispytest=True)
else:
with f:
f.write(data)
@@ -182,11 +210,11 @@ class LFPluginCollWrapper:
self.lfplugin = lfplugin
self._collected_at_least_one_failure = False
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector):
if isinstance(collector, Session):
out = yield
res = out.get_result() # type: CollectReport
res: CollectReport = out.get_result()
# Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths
@@ -229,11 +257,14 @@ class LFPluginCollSkipfiles:
def __init__(self, lfplugin: "LFPlugin") -> None:
self.lfplugin = lfplugin
@pytest.hookimpl
@hookimpl
def pytest_make_collect_report(
self, collector: nodes.Collector
) -> Optional[CollectReport]:
if isinstance(collector, Module):
# Packages are Modules, but _last_failed_paths only contains
# test-bearing paths and doesn't try to include the paths of their
# packages, so don't filter them.
if isinstance(collector, Module) and not isinstance(collector, Package):
if Path(str(collector.fspath)) not in self.lfplugin._last_failed_paths:
self.lfplugin._skipped_files += 1
@@ -251,11 +282,9 @@ class LFPlugin:
active_keys = "lf", "failedfirst"
self.active = any(config.getoption(key) for key in active_keys)
assert config.cache
self.lastfailed = config.cache.get(
"cache/lastfailed", {}
) # type: Dict[str, bool]
self._previously_failed_count = None # type: Optional[int]
self._report_status = None # type: Optional[str]
self.lastfailed: Dict[str, bool] = config.cache.get("cache/lastfailed", {})
self._previously_failed_count: Optional[int] = None
self._report_status: Optional[str] = None
self._skipped_files = 0 # count skipped files during collection due to --lf
if config.getoption("lf"):
@@ -290,7 +319,7 @@ class LFPlugin:
else:
self.lastfailed[report.nodeid] = True
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@hookimpl(hookwrapper=True, tryfirst=True)
def pytest_collection_modifyitems(
self, config: Config, items: List[nodes.Item]
) -> Generator[None, None, None]:
@@ -362,15 +391,15 @@ class NFPlugin:
assert config.cache is not None
self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@hookimpl(hookwrapper=True, tryfirst=True)
def pytest_collection_modifyitems(
self, items: List[nodes.Item]
) -> Generator[None, None, None]:
yield
if self.active:
new_items = order_preserving_dict() # type: Dict[str, nodes.Item]
other_items = order_preserving_dict() # type: Dict[str, nodes.Item]
new_items: Dict[str, nodes.Item] = {}
other_items: Dict[str, nodes.Item] = {}
for item in items:
if item.nodeid not in self.cached_nodeids:
new_items[item.nodeid] = item
@@ -385,7 +414,7 @@ class NFPlugin:
self.cached_nodeids.update(item.nodeid for item in items)
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True)
return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) # type: ignore[no-any-return]
def pytest_sessionfinish(self) -> None:
config = self.config
@@ -465,14 +494,14 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
return None
@pytest.hookimpl(tryfirst=True)
@hookimpl(tryfirst=True)
def pytest_configure(config: Config) -> None:
config.cache = Cache.for_config(config)
config.cache = Cache.for_config(config, _ispytest=True)
config.pluginmanager.register(LFPlugin(config), "lfplugin")
config.pluginmanager.register(NFPlugin(config), "nfplugin")
@pytest.fixture
@fixture
def cache(request: FixtureRequest) -> Cache:
"""Return a cache object that can persist state between testing sessions.
@@ -500,7 +529,7 @@ def pytest_report_header(config: Config) -> Optional[str]:
displaypath = cachedir.relative_to(config.rootpath)
except ValueError:
displaypath = cachedir
return "cachedir: {}".format(displaypath)
return f"cachedir: {displaypath}"
return None
@@ -542,5 +571,5 @@ def cacheshow(config: Config, session: Session) -> int:
# print("%s/" % p.relto(basedir))
if p.is_file():
key = str(p.relative_to(basedir))
tw.line("{} is a file of length {:d}".format(key, p.stat().st_size))
tw.line(f"{key} is a file of length {p.stat().st_size:d}")
return 0

View File

@@ -14,15 +14,18 @@ from typing import Iterator
from typing import Optional
from typing import TextIO
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import pytest
from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import SubRequest
from _pytest.nodes import Collector
from _pytest.nodes import File
from _pytest.nodes import Item
if TYPE_CHECKING:
@@ -113,11 +116,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
See https://github.com/pytest-dev/py/issues/103.
"""
if (
not sys.platform.startswith("win32")
or sys.version_info[:2] < (3, 6)
or hasattr(sys, "pypy_version_info")
):
if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):
return
# Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
@@ -149,7 +148,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
sys.stderr = _reopen_stdio(sys.stderr, "wb")
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_load_initial_conftests(early_config: Config):
ns = early_config.known_args_namespace
if ns.capture == "fd":
@@ -373,9 +372,7 @@ class FDCaptureBinary:
# Further complications are the need to support suspend() and the
# possibility of FD reuse (e.g. the tmpfile getting the very same
# target FD). The following approach is robust, I believe.
self.targetfd_invalid = os.open(
os.devnull, os.O_RDWR
) # type: Optional[int]
self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)
os.dup2(self.targetfd_invalid, targetfd)
else:
self.targetfd_invalid = None
@@ -386,8 +383,7 @@ class FDCaptureBinary:
self.syscapture = SysCapture(targetfd)
else:
self.tmpfile = EncodedFile(
# TODO: Remove type ignore, fixed in next mypy release.
TemporaryFile(buffering=0), # type: ignore[arg-type]
TemporaryFile(buffering=0),
encoding="utf-8",
errors="replace",
newline="",
@@ -504,13 +500,11 @@ class FDCapture(FDCaptureBinary):
class CaptureResult(Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`."""
# Can't use slots in Python<3.5.3 due to https://bugs.python.org/issue31272
if sys.version_info >= (3, 5, 3):
__slots__ = ("out", "err")
__slots__ = ("out", "err")
def __init__(self, out: AnyStr, err: AnyStr) -> None:
self.out = out # type: AnyStr
self.err = err # type: AnyStr
self.out: AnyStr = out
self.err: AnyStr = err
def __len__(self) -> int:
return 2
@@ -548,7 +542,7 @@ class CaptureResult(Generic[AnyStr]):
return tuple(self) < tuple(other)
def __repr__(self) -> str:
return "CaptureResult(out={!r}, err={!r})".format(self.out, self.err)
return f"CaptureResult(out={self.out!r}, err={self.err!r})"
class MultiCapture(Generic[AnyStr]):
@@ -642,7 +636,7 @@ def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
return MultiCapture(
in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True)
)
raise ValueError("unknown capturing method: {!r}".format(method))
raise ValueError(f"unknown capturing method: {method!r}")
# CaptureManager and CaptureFixture
@@ -669,8 +663,8 @@ class CaptureManager:
def __init__(self, method: "_CaptureMethod") -> None:
self._method = method
self._global_capturing = None # type: Optional[MultiCapture[str]]
self._capture_fixture = None # type: Optional[CaptureFixture[Any]]
self._global_capturing: Optional[MultiCapture[str]] = None
self._capture_fixture: Optional[CaptureFixture[Any]] = None
def __repr__(self) -> str:
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
@@ -793,9 +787,9 @@ class CaptureManager:
# Hooks
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: Collector):
if isinstance(collector, pytest.File):
if isinstance(collector, File):
self.resume_global_capture()
outcome = yield
self.suspend_global_capture()
@@ -808,38 +802,41 @@ class CaptureManager:
else:
yield
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("setup", item):
yield
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("call", item):
yield
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("teardown", item):
yield
@pytest.hookimpl(tryfirst=True)
@hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self) -> None:
self.stop_global_capturing()
@pytest.hookimpl(tryfirst=True)
@hookimpl(tryfirst=True)
def pytest_internalerror(self) -> None:
self.stop_global_capturing()
class CaptureFixture(Generic[AnyStr]):
"""Object returned by the :py:func:`capsys`, :py:func:`capsysbinary`,
:py:func:`capfd` and :py:func:`capfdbinary` fixtures."""
"""Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,
:fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
def __init__(self, captureclass, request: SubRequest) -> None:
def __init__(
self, captureclass, request: SubRequest, *, _ispytest: bool = False
) -> None:
check_ispytest(_ispytest)
self.captureclass = captureclass
self.request = request
self._capture = None # type: Optional[MultiCapture[AnyStr]]
self._capture: Optional[MultiCapture[AnyStr]] = None
self._captured_out = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER
@@ -902,7 +899,7 @@ class CaptureFixture(Generic[AnyStr]):
# The fixtures.
@pytest.fixture
@fixture
def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
@@ -911,7 +908,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
``out`` and ``err`` will be ``text`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](SysCapture, request)
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -919,7 +916,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
capman.unset_fixture()
@pytest.fixture
@fixture
def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
@@ -928,7 +925,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
``out`` and ``err`` will be ``bytes`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request)
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -936,7 +933,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
capman.unset_fixture()
@pytest.fixture
@fixture
def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
@@ -945,7 +942,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
``out`` and ``err`` will be ``text`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](FDCapture, request)
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -953,7 +950,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
capman.unset_fixture()
@pytest.fixture
@fixture
def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
@@ -962,7 +959,7 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
``out`` and ``err`` will be ``byte`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request)
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture

View File

@@ -2,18 +2,18 @@
import enum
import functools
import inspect
import os
import re
import sys
from contextlib import contextmanager
from inspect import Parameter
from inspect import signature
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Generic
from typing import Optional
from typing import overload as overload
from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
@@ -22,15 +22,8 @@ import attr
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
if sys.version_info < (3, 5, 2):
TYPE_CHECKING = False # type: bool
else:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import NoReturn
from typing import Type
from typing_extensions import Final
@@ -43,14 +36,9 @@ _S = TypeVar("_S")
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum):
token = 0
NOTSET = NotSetType.token # type: Final # noqa: E305
NOTSET: "Final" = NotSetType.token # noqa: E305
# fmt: on
MODULE_NOT_FOUND_ERROR = (
"ModuleNotFoundError" if sys.version_info[:2] >= (3, 6) else "ImportError"
)
if sys.version_info >= (3, 8):
from importlib import metadata as importlib_metadata
else:
@@ -65,18 +53,6 @@ def _format_args(func: Callable[..., Any]) -> str:
REGEX_TYPE = type(re.compile(""))
if sys.version_info < (3, 6):
def fspath(p):
"""os.fspath replacement, useful to point out when we should replace it by the
real function once we drop py35."""
return str(p)
else:
fspath = os.fspath
def is_generator(func: object) -> bool:
genfunc = inspect.isgeneratorfunction(func)
return genfunc and not iscoroutinefunction(func)
@@ -97,14 +73,10 @@ def iscoroutinefunction(func: object) -> bool:
def is_async_function(func: object) -> bool:
"""Return True if the given function seems to be an async function or
an async generator."""
return iscoroutinefunction(func) or (
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
)
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
def getlocation(function, curdir: Optional[str] = None) -> str:
from _pytest.pathlib import Path
function = get_real_func(function)
fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno
@@ -142,7 +114,7 @@ def getfuncargnames(
*,
name: str = "",
is_method: bool = False,
cls: Optional[type] = None
cls: Optional[type] = None,
) -> Tuple[str, ...]:
"""Return the names of a function's mandatory arguments.
@@ -171,17 +143,15 @@ def getfuncargnames(
parameters = signature(function).parameters
except (ValueError, TypeError) as e:
fail(
"Could not determine arguments of {!r}: {}".format(function, e),
pytrace=False,
f"Could not determine arguments of {function!r}: {e}", pytrace=False,
)
arg_names = tuple(
p.name
for p in parameters.values()
if (
# TODO: Remove type ignore after https://github.com/python/typeshed/pull/4383
p.kind is Parameter.POSITIONAL_OR_KEYWORD # type: ignore[unreachable]
or p.kind is Parameter.KEYWORD_ONLY # type: ignore[unreachable]
p.kind is Parameter.POSITIONAL_OR_KEYWORD
or p.kind is Parameter.KEYWORD_ONLY
)
and p.default is Parameter.empty
)
@@ -225,7 +195,7 @@ def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
_non_printable_ascii_translate_table = {
i: "\\x{:02x}".format(i) for i in range(128) if i not in range(32, 127)
i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127)
}
_non_printable_ascii_translate_table.update(
{ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"}
@@ -352,12 +322,6 @@ def safe_isclass(obj: object) -> bool:
return False
if sys.version_info < (3, 5, 2):
def overload(f): # noqa: F811
return f
if TYPE_CHECKING:
if sys.version_info >= (3, 8):
from typing import final as final
@@ -367,19 +331,15 @@ elif sys.version_info >= (3, 8):
from typing import final as final
else:
def final(f): # noqa: F811
def final(f):
return f
if getattr(attr, "__version_info__", ()) >= (19, 2):
ATTRS_EQ_FIELD = "eq"
else:
ATTRS_EQ_FIELD = "cmp"
if sys.version_info >= (3, 8):
from functools import cached_property as cached_property
else:
from typing import overload
from typing import Type
class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__")
@@ -390,35 +350,21 @@ else:
@overload
def __get__(
self, instance: None, owner: Optional["Type[_S]"] = ...
self, instance: None, owner: Optional[Type[_S]] = ...
) -> "cached_property[_S, _T]":
...
@overload # noqa: F811
def __get__( # noqa: F811
self, instance: _S, owner: Optional["Type[_S]"] = ...
) -> _T:
@overload
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T:
...
def __get__(self, instance, owner=None): # noqa: F811
def __get__(self, instance, owner=None):
if instance is None:
return self
value = instance.__dict__[self.func.__name__] = self.func(instance)
return value
# Sometimes an algorithm needs a dict which yields items in the order in which
# they were inserted when iterated. Since Python 3.7, `dict` preserves
# insertion order. Since `dict` is faster and uses less memory than
# `OrderedDict`, prefer to use it if possible.
if sys.version_info >= (3, 7):
order_preserving_dict = dict
else:
from collections import OrderedDict
order_preserving_dict = OrderedDict
# Perform exhaustiveness checking.
#
# Consider this example:

View File

@@ -12,6 +12,7 @@ import sys
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
@@ -26,6 +27,8 @@ from typing import Sequence
from typing import Set
from typing import TextIO
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import attr
@@ -45,18 +48,15 @@ from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.compat import importlib_metadata
from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail
from _pytest.outcomes import Skipped
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode
from _pytest.pathlib import Path
from _pytest.store import Store
from _pytest.warning_types import PytestConfigWarning
if TYPE_CHECKING:
from typing import Type
from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter
@@ -104,7 +104,7 @@ class ConftestImportFailure(Exception):
def __init__(
self,
path: py.path.local,
excinfo: Tuple["Type[Exception]", Exception, TracebackType],
excinfo: Tuple[Type[Exception], Exception, TracebackType],
) -> None:
super().__init__(path, excinfo)
self.path = path
@@ -144,9 +144,7 @@ def main(
except ConftestImportFailure as e:
exc_info = ExceptionInfo(e.excinfo)
tw = TerminalWriter(sys.stderr)
tw.line(
"ImportError while loading conftest '{e.path}'.".format(e=e), red=True
)
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
exc_info.traceback = exc_info.traceback.filter(
filter_traceback_for_conftest_import_failure
)
@@ -161,9 +159,9 @@ def main(
return ExitCode.USAGE_ERROR
else:
try:
ret = config.hook.pytest_cmdline_main(
ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main(
config=config
) # type: Union[ExitCode, int]
)
try:
return ExitCode(ret)
except ValueError:
@@ -173,7 +171,7 @@ def main(
except UsageError as e:
tw = TerminalWriter(sys.stderr)
for msg in e.args:
tw.line("ERROR: {}\n".format(msg), red=True)
tw.line(f"ERROR: {msg}\n", red=True)
return ExitCode.USAGE_ERROR
@@ -206,7 +204,7 @@ def filename_arg(path: str, optname: str) -> str:
:optname: Name of the option.
"""
if os.path.isdir(path):
raise UsageError("{} must be a filename, given: {}".format(optname, path))
raise UsageError(f"{optname} must be a filename, given: {path}")
return path
@@ -217,7 +215,7 @@ def directory_arg(path: str, optname: str) -> str:
:optname: Name of the option.
"""
if not os.path.isdir(path):
raise UsageError("{} must be a directory, given: {}".format(optname, path))
raise UsageError(f"{optname} must be a directory, given: {path}")
return path
@@ -253,11 +251,13 @@ default_plugins = essential_plugins + (
"warnings",
"logging",
"reports",
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
"faulthandler",
)
builtin_plugins = set(default_plugins)
builtin_plugins.add("pytester")
builtin_plugins.add("pytester_assertions")
def get_config(
@@ -339,27 +339,27 @@ class PytestPluginManager(PluginManager):
super().__init__("pytest")
# The objects are module objects, only used generically.
self._conftest_plugins = set() # type: Set[types.ModuleType]
self._conftest_plugins: Set[types.ModuleType] = set()
# State related to local conftest plugins.
self._dirpath2confmods = {} # type: Dict[py.path.local, List[types.ModuleType]]
self._conftestpath2mod = {} # type: Dict[Path, types.ModuleType]
self._confcutdir = None # type: Optional[py.path.local]
self._dirpath2confmods: Dict[py.path.local, List[types.ModuleType]] = {}
self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
self._confcutdir: Optional[py.path.local] = None
self._noconftest = False
self._duplicatepaths = set() # type: Set[py.path.local]
self._duplicatepaths: Set[py.path.local] = set()
# plugins that were explicitly skipped with pytest.skip
# list of (module name, skip reason)
# previously we would issue a warning when a plugin was skipped, but
# since we refactored warnings as first citizens of Config, they are
# just stored here to be used later.
self.skipped_plugins = [] # type: List[Tuple[str, str]]
self.skipped_plugins: List[Tuple[str, str]] = []
self.add_hookspecs(_pytest.hookspec)
self.register(self)
if os.environ.get("PYTEST_DEBUG"):
err = sys.stderr # type: IO[str]
encoding = getattr(err, "encoding", "utf8") # type: str
err: IO[str] = sys.stderr
encoding: str = getattr(err, "encoding", "utf8")
try:
err = open(
os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding,
@@ -433,7 +433,7 @@ class PytestPluginManager(PluginManager):
)
)
return None
ret = super().register(plugin, name) # type: Optional[str]
ret: Optional[str] = super().register(plugin, name)
if ret:
self.hook.pytest_plugin_registered.call_historic(
kwargs=dict(plugin=plugin, manager=self)
@@ -445,7 +445,7 @@ class PytestPluginManager(PluginManager):
def getplugin(self, name: str):
# Support deprecated naming because plugins (xdist e.g.) use it.
plugin = self.get_plugin(name) # type: Optional[_PluggyPlugin]
plugin: Optional[_PluggyPlugin] = self.get_plugin(name)
return plugin
def hasplugin(self, name: str) -> bool:
@@ -583,7 +583,7 @@ class PytestPluginManager(PluginManager):
if path and path.relto(dirpath) or path == dirpath:
assert mod not in mods
mods.append(mod)
self.trace("loading conftestmodule {!r}".format(mod))
self.trace(f"loading conftestmodule {mod!r}")
self.consider_conftest(mod)
return mod
@@ -866,7 +866,7 @@ class Config:
self,
pluginmanager: PytestPluginManager,
*,
invocation_params: Optional[InvocationParams] = None
invocation_params: Optional[InvocationParams] = None,
) -> None:
from .argparsing import Parser, FILE_OR_DIR
@@ -889,7 +889,7 @@ class Config:
_a = FILE_OR_DIR
self._parser = Parser(
usage="%(prog)s [options] [{}] [{}] [...]".format(_a, _a),
usage=f"%(prog)s [options] [{_a}] [{_a}] [...]",
processopt=self._processopt,
)
self.pluginmanager = pluginmanager
@@ -900,10 +900,10 @@ class Config:
self.trace = self.pluginmanager.trace.root.get("config")
self.hook = self.pluginmanager.hook
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._inicache: Dict[str, Any] = {}
self._override_ini: Sequence[str] = ()
self._opt2dest: Dict[str, str] = {}
self._cleanup: List[Callable[[], None]] = []
# A place where plugins can store information on the config for their
# own use. Currently only intended for internal plugins.
self._store = Store()
@@ -916,7 +916,7 @@ class Config:
if TYPE_CHECKING:
from _pytest.cacheprovider import Cache
self.cache = None # type: Optional[Cache]
self.cache: Optional[Cache] = None
@property
def invocation_dir(self) -> py.path.local:
@@ -991,9 +991,9 @@ class Config:
fin()
def get_terminal_writer(self) -> TerminalWriter:
terminalreporter = self.pluginmanager.get_plugin(
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
"terminalreporter"
) # type: TerminalReporter
)
return terminalreporter._tw
def pytest_cmdline_parse(
@@ -1028,7 +1028,7 @@ class Config:
option: Optional[argparse.Namespace] = None,
) -> None:
if option and getattr(option, "fulltrace", False):
style = "long" # type: _TracebackStyle
style: _TracebackStyle = "long"
else:
style = "native"
excrepr = excinfo.getrepr(
@@ -1179,6 +1179,11 @@ class Config:
self._validate_plugins()
self._warn_about_skipped_plugins()
if self.known_args_namespace.strict:
self.issue_config_time_warning(
_pytest.deprecated.STRICT_OPTION, stacklevel=2
)
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
confcutdir = str(self.inipath.parent)
self.known_args_namespace.confcutdir = confcutdir
@@ -1191,9 +1196,7 @@ class Config:
# we don't want to prevent --help/--version to work
# so just let is pass and print a warning at the end
self.issue_config_time_warning(
PytestConfigWarning(
"could not load initial conftests: {}".format(e.path)
),
PytestConfigWarning(f"could not load initial conftests: {e.path}"),
stacklevel=2,
)
else:
@@ -1227,7 +1230,7 @@ class Config:
def _validate_config_options(self) -> None:
for key in sorted(self._get_unknown_ini_keys()):
self._warn_or_fail_if_strict("Unknown config option: {}\n".format(key))
self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")
def _validate_plugins(self) -> None:
required_plugins = sorted(self.getini("required_plugins"))
@@ -1362,7 +1365,7 @@ class Config:
try:
description, type, default = self._parser._inidict[name]
except KeyError as e:
raise ValueError("unknown configuration value: {!r}".format(name)) from e
raise ValueError(f"unknown configuration value: {name!r}") from e
override_value = self._get_override_ini_value(name)
if override_value is None:
try:
@@ -1406,7 +1409,7 @@ class Config:
elif type == "bool":
return _strtobool(str(value).strip())
else:
assert type is None
assert type in [None, "string"]
return value
def _getconftest_pathlist(
@@ -1419,7 +1422,7 @@ class Config:
except KeyError:
return None
modpath = py.path.local(mod.__file__).dirpath()
values = [] # type: List[py.path.local]
values: List[py.path.local] = []
for relroot in relroots:
if not isinstance(relroot, py.path.local):
relroot = relroot.replace("/", os.sep)
@@ -1467,8 +1470,8 @@ class Config:
if skip:
import pytest
pytest.skip("no {!r} option found".format(name))
raise ValueError("no option named {!r}".format(name)) from e
pytest.skip(f"no {name!r} option found")
raise ValueError(f"no option named {name!r}") from e
def getvalue(self, name: str, path=None):
"""Deprecated, use getoption() instead."""
@@ -1501,7 +1504,7 @@ class Config:
def _warn_about_skipped_plugins(self) -> None:
for module_name, msg in self.pluginmanager.skipped_plugins:
self.issue_config_time_warning(
PytestConfigWarning("skipped plugin {!r}: {}".format(module_name, msg)),
PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"),
stacklevel=2,
)
@@ -1554,13 +1557,13 @@ def _strtobool(val: str) -> bool:
elif val in ("n", "no", "f", "false", "off", "0"):
return False
else:
raise ValueError("invalid truth value {!r}".format(val))
raise ValueError(f"invalid truth value {val!r}")
@lru_cache(maxsize=50)
def parse_warning_filter(
arg: str, *, escape: bool
) -> "Tuple[str, str, Type[Warning], str, int]":
) -> Tuple[str, str, Type[Warning], str, int]:
"""Parse a warnings filter string.
This is copied from warnings._setoption, but does not apply the filter,
@@ -1568,14 +1571,12 @@ def parse_warning_filter(
"""
parts = arg.split(":")
if len(parts) > 5:
raise warnings._OptionError("too many fields (max 5): {!r}".format(arg))
raise warnings._OptionError(f"too many fields (max 5): {arg!r}")
while len(parts) < 5:
parts.append("")
action_, message, category_, module, lineno_ = [s.strip() for s in parts]
action = warnings._getaction(action_) # type: str # type: ignore[attr-defined]
category = warnings._getcategory(
category_
) # type: Type[Warning] # type: ignore[attr-defined]
action: str = warnings._getaction(action_) # type: ignore[attr-defined]
category: Type[Warning] = warnings._getcategory(category_) # type: ignore[attr-defined]
if message and escape:
message = re.escape(message)
if module and escape:
@@ -1586,7 +1587,7 @@ def parse_warning_filter(
if lineno < 0:
raise ValueError
except (ValueError, OverflowError) as e:
raise warnings._OptionError("invalid lineno {!r}".format(lineno_)) from e
raise warnings._OptionError(f"invalid lineno {lineno_!r}") from e
else:
lineno = 0
return action, message, category, module, lineno

View File

@@ -11,13 +11,13 @@ from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import py
import _pytest._io
from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config.exceptions import UsageError
if TYPE_CHECKING:
@@ -35,7 +35,7 @@ class Parser:
there's an error processing the command line arguments.
"""
prog = None # type: Optional[str]
prog: Optional[str] = None
def __init__(
self,
@@ -43,12 +43,12 @@ class Parser:
processopt: Optional[Callable[["Argument"], None]] = None,
) -> None:
self._anonymous = OptionGroup("custom options", parser=self)
self._groups = [] # type: List[OptionGroup]
self._groups: List[OptionGroup] = []
self._processopt = processopt
self._usage = usage
self._inidict = {} # type: Dict[str, Tuple[str, Optional[str], Any]]
self._ininames = [] # type: List[str]
self.extra_info = {} # type: Dict[str, Any]
self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {}
self._ininames: List[str] = []
self.extra_info: Dict[str, Any] = {}
def processoption(self, option: "Argument") -> None:
if self._processopt:
@@ -160,20 +160,23 @@ class Parser:
self,
name: str,
help: str,
type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None,
type: Optional[
"Literal['string', 'pathlist', 'args', 'linelist', 'bool']"
] = None,
default=None,
) -> None:
"""Register an ini-file option.
:name: Name of the ini-variable.
:type: Type of the variable, can be ``pathlist``, ``args``, ``linelist``
or ``bool``.
:type: Type of the variable, can be ``string``, ``pathlist``, ``args``,
``linelist`` or ``bool``. Defaults to ``string`` if ``None`` or
not passed.
:default: Default value if no ini-file option exists but is queried.
The value of ini-variables can be retrieved via a call to
:py:func:`config.getini(name) <_pytest.config.Config.getini>`.
"""
assert type in (None, "pathlist", "args", "linelist", "bool")
assert type in (None, "string", "pathlist", "args", "linelist", "bool")
self._inidict[name] = (help, type, default)
self._ininames.append(name)
@@ -188,7 +191,7 @@ class ArgumentError(Exception):
def __str__(self) -> str:
if self.option_id:
return "option {}: {}".format(self.option_id, self.msg)
return f"option {self.option_id}: {self.msg}"
else:
return self.msg
@@ -207,8 +210,8 @@ class Argument:
def __init__(self, *names: str, **attrs: Any) -> None:
"""Store parms in private vars for use in add_argument."""
self._attrs = attrs
self._short_opts = [] # type: List[str]
self._long_opts = [] # type: List[str]
self._short_opts: List[str] = []
self._long_opts: List[str] = []
if "%default" in (attrs.get("help") or ""):
warnings.warn(
'pytest now uses argparse. "%default" should be'
@@ -254,7 +257,7 @@ class Argument:
except KeyError:
pass
self._set_opt_strings(names)
dest = attrs.get("dest") # type: Optional[str]
dest: Optional[str] = attrs.get("dest")
if dest:
self.dest = dest
elif self._long_opts:
@@ -315,7 +318,7 @@ class Argument:
self._long_opts.append(opt)
def __repr__(self) -> str:
args = [] # type: List[str]
args: List[str] = []
if self._short_opts:
args += ["_short_opts: " + repr(self._short_opts)]
if self._long_opts:
@@ -334,7 +337,7 @@ class OptionGroup:
) -> None:
self.name = name
self.description = description
self.options = [] # type: List[Argument]
self.options: List[Argument] = []
self.parser = parser
def addoption(self, *optnames: str, **attrs: Any) -> None:
@@ -389,11 +392,11 @@ class MyOptionParser(argparse.ArgumentParser):
def error(self, message: str) -> "NoReturn":
"""Transform argparse error message into UsageError."""
msg = "{}: error: {}".format(self.prog, message)
msg = f"{self.prog}: error: {message}"
if hasattr(self._parser, "_config_source_hint"):
# Type ignored because the attribute is set dynamically.
msg = "{} ({})".format(msg, self._parser._config_source_hint) # type: ignore
msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore
raise UsageError(self.format_usage() + msg)
@@ -410,7 +413,7 @@ class MyOptionParser(argparse.ArgumentParser):
if arg and arg[0] == "-":
lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))]
for k, v in sorted(self.extra_info.items()):
lines.append(" {}: {}".format(k, v))
lines.append(f" {k}: {v}")
self.error("\n".join(lines))
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
return parsed
@@ -472,9 +475,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
if orgstr and orgstr[0] != "-": # only optional arguments
return orgstr
res = getattr(
action, "_formatted_action_invocation", None
) # type: Optional[str]
res: Optional[str] = getattr(action, "_formatted_action_invocation", None)
if res:
return res
options = orgstr.split(", ")
@@ -483,7 +484,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
action._formatted_action_invocation = orgstr # type: ignore
return orgstr
return_list = []
short_long = {} # type: Dict[str, str]
short_long: Dict[str, str] = {}
for option in options:
if len(option) == 2 or option[2] == " ":
continue

View File

@@ -1,21 +1,20 @@
import itertools
import os
from pathlib import Path
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import iniconfig
from .exceptions import UsageError
from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath
from _pytest.pathlib import commonpath
from _pytest.pathlib import Path
if TYPE_CHECKING:
from . import Config
@@ -28,7 +27,7 @@ def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
Raise UsageError if the file cannot be parsed.
"""
try:
return iniconfig.IniConfig(path)
return iniconfig.IniConfig(str(path))
except iniconfig.ParseError as exc:
raise UsageError(str(exc)) from exc
@@ -100,7 +99,7 @@ def locate_config(
args = [Path.cwd()]
for arg in args:
argpath = absolutepath(arg)
for base in itertools.chain((argpath,), reversed(argpath.parents)):
for base in (argpath, *argpath.parents):
for config_name in config_names:
p = base / config_name
if p.is_file():
@@ -111,7 +110,7 @@ def locate_config(
def get_common_ancestor(paths: Iterable[Path]) -> Path:
common_ancestor = None # type: Optional[Path]
common_ancestor: Optional[Path] = None
for path in paths:
if not path.exists():
continue
@@ -176,7 +175,7 @@ def determine_setup(
dirs = get_dirs_from_args(args)
if inifile:
inipath_ = absolutepath(inifile)
inipath = inipath_ # type: Optional[Path]
inipath: Optional[Path] = inipath_
inicfg = load_config_dict_from_file(inipath_) or {}
if rootdir_cmd_arg is None:
rootdir = get_common_ancestor(dirs)
@@ -184,9 +183,7 @@ def determine_setup(
ancestor = get_common_ancestor(dirs)
rootdir, inipath, inicfg = locate_config([ancestor])
if rootdir is None and rootdir_cmd_arg is None:
for possible_rootdir in itertools.chain(
(ancestor,), reversed(ancestor.parents)
):
for possible_rootdir in (ancestor, *ancestor.parents):
if (possible_rootdir / "setup.py").is_file():
rootdir = possible_rootdir
break

View File

@@ -9,11 +9,12 @@ from typing import Generator
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
from _pytest import outcomes
from _pytest._code import ExceptionInfo
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.config import hookimpl
@@ -24,8 +25,6 @@ from _pytest.nodes import Node
from _pytest.reports import BaseReport
if TYPE_CHECKING:
from typing import Type
from _pytest.capture import CaptureManager
from _pytest.runner import CallInfo
@@ -36,7 +35,7 @@ def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
modname, classname = value.split(":")
except ValueError as e:
raise argparse.ArgumentTypeError(
"{!r} is not in the format 'modname:classname'".format(value)
f"{value!r} is not in the format 'modname:classname'"
) from e
return (modname, classname)
@@ -95,13 +94,13 @@ def pytest_configure(config: Config) -> None:
class pytestPDB:
"""Pseudo PDB that defers to the real pdb."""
_pluginmanager = None # type: Optional[PytestPluginManager]
_config = None # type: Config
_saved = (
[]
) # type: List[Tuple[Callable[..., None], Optional[PytestPluginManager], Config]]
_pluginmanager: Optional[PytestPluginManager] = None
_config: Optional[Config] = None
_saved: List[
Tuple[Callable[..., None], Optional[PytestPluginManager], Optional[Config]]
] = []
_recursive_debug = 0
_wrapped_pdb_cls = None # type: Optional[Tuple[Type[Any], Type[Any]]]
_wrapped_pdb_cls: Optional[Tuple[Type[Any], Type[Any]]] = None
@classmethod
def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]:
@@ -137,7 +136,7 @@ class pytestPDB:
except Exception as exc:
value = ":".join((modname, classname))
raise UsageError(
"--pdbcls: could not import {!r}: {}".format(value, exc)
f"--pdbcls: could not import {value!r}: {exc}"
) from exc
else:
import pdb
@@ -167,6 +166,7 @@ class pytestPDB:
def do_continue(self, arg):
ret = super().do_continue(arg)
if cls._recursive_debug == 0:
assert cls._config is not None
tw = _pytest.config.create_terminal_writer(cls._config)
tw.line()
@@ -240,7 +240,7 @@ class pytestPDB:
import _pytest.config
if cls._pluginmanager is None:
capman = None # type: Optional[CaptureManager]
capman: Optional[CaptureManager] = None
else:
capman = cls._pluginmanager.getplugin("capturemanager")
if capman:
@@ -258,7 +258,7 @@ class pytestPDB:
else:
capturing = cls._is_capturing(capman)
if capturing == "global":
tw.sep(">", "PDB {} (IO-capturing turned off)".format(method))
tw.sep(">", f"PDB {method} (IO-capturing turned off)")
elif capturing:
tw.sep(
">",
@@ -266,7 +266,7 @@ class pytestPDB:
% (method, capturing),
)
else:
tw.sep(">", "PDB {}".format(method))
tw.sep(">", f"PDB {method}")
_pdb = cls._import_pdb_cls(capman)(**kwargs)

View File

@@ -8,6 +8,8 @@ All constants defined in this module should be either instances of
:class:`PytestWarning`, or :class:`UnformattedWarning`
in case of warnings which need to format their messages.
"""
from warnings import warn
from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import UnformattedWarning
@@ -20,9 +22,10 @@ DEPRECATED_EXTERNAL_PLUGINS = {
}
FILLFUNCARGS = PytestDeprecationWarning(
"The `_fillfuncargs` function is deprecated, use "
"function._request._fillfixtures() instead if you cannot avoid reaching into internals."
FILLFUNCARGS = UnformattedWarning(
PytestDeprecationWarning,
"{name} is deprecated, use "
"function._request._fillfixtures() instead if you cannot avoid reaching into internals.",
)
PYTEST_COLLECT_MODULE = UnformattedWarning(
@@ -31,6 +34,10 @@ PYTEST_COLLECT_MODULE = UnformattedWarning(
"Please update to the new name.",
)
YIELD_FIXTURE = PytestDeprecationWarning(
"@pytest.yield_fixture is deprecated.\n"
"Use @pytest.fixture instead; they are the same."
)
MINUS_K_DASH = PytestDeprecationWarning(
"The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead."
@@ -50,3 +57,31 @@ FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestDeprecationWarning(
"The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; "
"use self.session.gethookproxy() and self.session.isinitpath() instead. "
)
STRICT_OPTION = PytestDeprecationWarning(
"The --strict option is deprecated, use --strict-markers instead."
)
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
# You want to make some `__init__` or function "private".
#
# def my_private_function(some, args):
# ...
#
# Do this:
#
# def my_private_function(some, args, *, _ispytest: bool = False):
# check_ispytest(_ispytest)
# ...
#
# Change all internal/allowed calls to
#
# my_private_function(some, args, _ispytest=True)
#
# All other calls will get the default _ispytest=False and trigger
# the warning (possibly error in the future).
def check_ispytest(ispytest: bool) -> None:
if not ispytest:
warn(PRIVATE, stacklevel=3)

View File

@@ -17,6 +17,8 @@ from typing import Optional
from typing import Pattern
from typing import Sequence
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import py.path
@@ -28,7 +30,6 @@ from _pytest._code.code import ReprFileLocation
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest.compat import safe_getattr
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureRequest
@@ -40,7 +41,6 @@ from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
import doctest
from typing import Type
DOCTEST_REPORT_CHOICE_NONE = "none"
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
@@ -59,7 +59,7 @@ DOCTEST_REPORT_CHOICES = (
# Lazy definition of runner class
RUNNER_CLASS = None
# Lazy definition of output checker class
CHECKER_CLASS = None # type: Optional[Type[doctest.OutputChecker]]
CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None
def pytest_addoption(parser: Parser) -> None:
@@ -124,10 +124,10 @@ def pytest_collect_file(
config = parent.config
if path.ext == ".py":
if config.option.doctestmodules and not _is_setup_py(path):
mod = DoctestModule.from_parent(parent, fspath=path) # type: DoctestModule
mod: DoctestModule = DoctestModule.from_parent(parent, fspath=path)
return mod
elif _is_doctest(config, path, parent):
txt = DoctestTextfile.from_parent(parent, fspath=path) # type: DoctestTextfile
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, fspath=path)
return txt
return None
@@ -163,12 +163,12 @@ class ReprFailDoctest(TerminalRepr):
class MultipleDoctestFailures(Exception):
def __init__(self, failures: "Sequence[doctest.DocTestFailure]") -> None:
def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
super().__init__()
self.failures = failures
def _init_runner_class() -> "Type[doctest.DocTestRunner]":
def _init_runner_class() -> Type["doctest.DocTestRunner"]:
import doctest
class PytestDoctestRunner(doctest.DebugRunner):
@@ -180,7 +180,7 @@ def _init_runner_class() -> "Type[doctest.DocTestRunner]":
def __init__(
self,
checker: Optional[doctest.OutputChecker] = None,
checker: Optional["doctest.OutputChecker"] = None,
verbose: Optional[bool] = None,
optionflags: int = 0,
continue_on_failure: bool = True,
@@ -204,7 +204,7 @@ def _init_runner_class() -> "Type[doctest.DocTestRunner]":
out,
test: "doctest.DocTest",
example: "doctest.Example",
exc_info: "Tuple[Type[BaseException], BaseException, types.TracebackType]",
exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
) -> None:
if isinstance(exc_info[1], OutcomeException):
raise exc_info[1]
@@ -251,7 +251,7 @@ class DoctestItem(pytest.Item):
self.runner = runner
self.dtest = dtest
self.obj = None
self.fixture_request = None # type: Optional[FixtureRequest]
self.fixture_request: Optional[FixtureRequest] = None
@classmethod
def from_parent( # type: ignore
@@ -260,7 +260,7 @@ class DoctestItem(pytest.Item):
*,
name: str,
runner: "doctest.DocTestRunner",
dtest: "doctest.DocTest"
dtest: "doctest.DocTest",
):
# incompatible signature due to to imposed limits on sublcass
"""The public named constructor."""
@@ -281,7 +281,7 @@ class DoctestItem(pytest.Item):
assert self.runner is not None
_check_all_skipped(self.dtest)
self._disable_output_capturing_for_darwin()
failures = [] # type: List[doctest.DocTestFailure]
failures: List["doctest.DocTestFailure"] = []
# Type ignored because we change the type of `out` from what
# doctest expects.
self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
@@ -305,9 +305,9 @@ class DoctestItem(pytest.Item):
) -> Union[str, TerminalRepr]:
import doctest
failures = (
None
) # type: Optional[Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]]
failures: Optional[
Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
] = (None)
if isinstance(
excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
):
@@ -349,7 +349,7 @@ class DoctestItem(pytest.Item):
]
indent = ">>>"
for line in example.source.splitlines():
lines.append("??? {} {}".format(indent, line))
lines.append(f"??? {indent} {line}")
indent = "..."
if isinstance(failure, doctest.DocTestFailure):
lines += checker.output_difference(
@@ -563,12 +563,12 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
node=doctest_item, func=func, cls=None, funcargs=False
)
fixture_request = FixtureRequest(doctest_item)
fixture_request = FixtureRequest(doctest_item, _ispytest=True)
fixture_request._fillfixtures()
return fixture_request
def _init_checker_class() -> "Type[doctest.OutputChecker]":
def _init_checker_class() -> Type["doctest.OutputChecker"]:
import doctest
import re
@@ -636,8 +636,8 @@ def _init_checker_class() -> "Type[doctest.OutputChecker]":
return got
offset = 0
for w, g in zip(wants, gots):
fraction = w.group("fraction") # type: Optional[str]
exponent = w.group("exponent1") # type: Optional[str]
fraction: Optional[str] = w.group("fraction")
exponent: Optional[str] = w.group("exponent1")
if exponent is None:
exponent = w.group("exponent2")
if fraction is None:

View File

@@ -69,7 +69,12 @@ class FaultHandlerHooks:
@staticmethod
def _get_stderr_fileno():
try:
return sys.stderr.fileno()
fileno = sys.stderr.fileno()
# The Twisted Logger will return an invalid file descriptor since it is not backed
# by an FD. So, let's also forward this to the same code path as with pytest-xdist.
if fileno == -1:
raise AttributeError()
return fileno
except (AttributeError, io.UnsupportedOperation):
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors

View File

@@ -16,9 +16,12 @@ from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
@@ -26,12 +29,14 @@ import attr
import py
import _pytest
from _pytest import nodes
from _pytest._code import getfslineno
from _pytest._code.code import FormattedExcinfo
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest.compat import _format_args
from _pytest.compat import _PytestWrapper
from _pytest.compat import assert_never
from _pytest.compat import final
from _pytest.compat import get_real_func
from _pytest.compat import get_real_method
@@ -40,27 +45,26 @@ from _pytest.compat import getimfunc
from _pytest.compat import getlocation
from _pytest.compat import is_generator
from _pytest.compat import NOTSET
from _pytest.compat import order_preserving_dict
from _pytest.compat import overload
from _pytest.compat import safe_getattr
from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import FILLFUNCARGS
from _pytest.deprecated import YIELD_FIXTURE
from _pytest.mark import Mark
from _pytest.mark import ParameterSet
from _pytest.mark.structures import MarkDecorator
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
from _pytest.pathlib import absolutepath
from _pytest.store import StoreKey
if TYPE_CHECKING:
from typing import Deque
from typing import NoReturn
from typing import Type
from typing_extensions import Literal
from _pytest import nodes
from _pytest.main import Session
from _pytest.python import CallSpec2
from _pytest.python import Function
@@ -91,7 +95,7 @@ _FixtureCachedResult = Union[
# Cache key.
object,
# Exc info if raised.
Tuple["Type[BaseException]", BaseException, TracebackType],
Tuple[Type[BaseException], BaseException, TracebackType],
],
]
@@ -103,24 +107,9 @@ class PseudoFixtureDef(Generic[_FixtureValue]):
def pytest_sessionstart(session: "Session") -> None:
import _pytest.python
import _pytest.nodes
scopename2class.update(
{
"package": _pytest.python.Package,
"class": _pytest.python.Class,
"module": _pytest.python.Module,
"function": _pytest.nodes.Item,
"session": _pytest.main.Session,
}
)
session._fixturemanager = FixtureManager(session)
scopename2class = {} # type: Dict[str, Type[nodes.Node]]
def get_scope_package(node, fixturedef: "FixtureDef[object]"):
import pytest
@@ -136,15 +125,31 @@ def get_scope_package(node, fixturedef: "FixtureDef[object]"):
return current
def get_scope_node(node, scope):
cls = scopename2class.get(scope)
if cls is None:
raise ValueError("unknown scope")
return node.getparent(cls)
def get_scope_node(
node: nodes.Node, scope: "_Scope"
) -> Optional[Union[nodes.Item, nodes.Collector]]:
import _pytest.python
if scope == "function":
return node.getparent(nodes.Item)
elif scope == "class":
return node.getparent(_pytest.python.Class)
elif scope == "module":
return node.getparent(_pytest.python.Module)
elif scope == "package":
return node.getparent(_pytest.python.Package)
elif scope == "session":
return node.getparent(_pytest.main.Session)
else:
assert_never(scope)
# Used for storing artificial fixturedefs for direct parametrization.
name2pseudofixturedef_key = StoreKey[Dict[str, "FixtureDef[Any]"]]()
def add_funcarg_pseudo_fixture_def(
collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
) -> None:
# This function will transform all collected calls to functions
# if they use direct funcargs (i.e. direct parametrization)
@@ -156,8 +161,8 @@ def add_funcarg_pseudo_fixture_def(
# This function call does not have direct parametrization.
return
# Collect funcargs of all callspecs into a list of values.
arg2params = {} # type: Dict[str, List[object]]
arg2scope = {} # type: Dict[str, _Scope]
arg2params: Dict[str, List[object]] = {}
arg2scope: Dict[str, _Scope] = {}
for callspec in metafunc._calls:
for argname, argvalue in callspec.funcargs.items():
assert argname not in callspec.params
@@ -186,8 +191,15 @@ def add_funcarg_pseudo_fixture_def(
assert scope == "class" and isinstance(collector, _pytest.python.Module)
# Use module-level collector for class-scope (for now).
node = collector
if node and argname in node._name2pseudofixturedef:
arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]]
if node is None:
name2pseudofixturedef = None
else:
default: Dict[str, FixtureDef[Any]] = {}
name2pseudofixturedef = node._store.setdefault(
name2pseudofixturedef_key, default
)
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
arg2fixturedefs[argname] = [name2pseudofixturedef[argname]]
else:
fixturedef = FixtureDef(
fixturemanager=fixturemanager,
@@ -200,17 +212,17 @@ def add_funcarg_pseudo_fixture_def(
ids=None,
)
arg2fixturedefs[argname] = [fixturedef]
if node is not None:
node._name2pseudofixturedef[argname] = fixturedef
if name2pseudofixturedef is not None:
name2pseudofixturedef[argname] = fixturedef
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
"""Return fixturemarker or None if it doesn't exist or raised
exceptions."""
try:
fixturemarker = getattr(
fixturemarker: Optional[FixtureFunctionMarker] = getattr(
obj, "_pytestfixturefunction", None
) # type: Optional[FixtureFunctionMarker]
)
except TEST_OUTCOME:
# some objects raise errors like request (from flask import request)
# we don't expect them to be fixture functions
@@ -222,7 +234,7 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
_Key = Tuple[object, ...]
def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator[_Key]:
def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_Key]:
"""Return list of keys for all parametrized arguments which match
the specified scope. """
assert scopenum < scopenum_function # function
@@ -231,7 +243,7 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator
except AttributeError:
pass
else:
cs = callspec # type: CallSpec2
cs: CallSpec2 = callspec
# cs.indices.items() is random order of argnames. Need to
# sort this so that different calls to
# get_parametrized_fixture_keys will be deterministic.
@@ -239,7 +251,7 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator
if cs._arg2scopenum[argname] != scopenum:
continue
if scopenum == 0: # session
key = (argname, param_index) # type: _Key
key: _Key = (argname, param_index)
elif scopenum == 1: # package
key = (argname, param_index, item.fspath.dirpath())
elif scopenum == 2: # module
@@ -256,37 +268,28 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator
# setups and teardowns.
def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]":
argkeys_cache = {} # type: Dict[int, Dict[nodes.Item, Dict[_Key, None]]]
items_by_argkey = {} # type: Dict[int, Dict[_Key, Deque[nodes.Item]]]
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]] = {}
items_by_argkey: Dict[int, Dict[_Key, Deque[nodes.Item]]] = {}
for scopenum in range(0, scopenum_function):
d = {} # type: Dict[nodes.Item, Dict[_Key, None]]
d: Dict[nodes.Item, Dict[_Key, None]] = {}
argkeys_cache[scopenum] = d
item_d = defaultdict(deque) # type: Dict[_Key, Deque[nodes.Item]]
item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque)
items_by_argkey[scopenum] = item_d
for item in items:
# cast is a workaround for https://github.com/python/typeshed/issues/3800.
keys = cast(
"Dict[_Key, None]",
order_preserving_dict.fromkeys(
get_parametrized_fixture_keys(item, scopenum), None
),
)
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scopenum), None)
if keys:
d[item] = keys
for key in keys:
item_d[key].append(item)
# cast is a workaround for https://github.com/python/typeshed/issues/3800.
items_dict = cast(
"Dict[nodes.Item, None]", order_preserving_dict.fromkeys(items, None)
)
items_dict = dict.fromkeys(items, None)
return list(reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, 0))
def fix_cache_order(
item: "nodes.Item",
argkeys_cache: "Dict[int, Dict[nodes.Item, Dict[_Key, None]]]",
items_by_argkey: "Dict[int, Dict[_Key, Deque[nodes.Item]]]",
item: nodes.Item,
argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]],
items_by_argkey: Dict[int, Dict[_Key, "Deque[nodes.Item]"]],
) -> None:
for scopenum in range(0, scopenum_function):
for key in argkeys_cache[scopenum].get(item, []):
@@ -294,26 +297,26 @@ def fix_cache_order(
def reorder_items_atscope(
items: "Dict[nodes.Item, None]",
argkeys_cache: "Dict[int, Dict[nodes.Item, Dict[_Key, None]]]",
items_by_argkey: "Dict[int, Dict[_Key, Deque[nodes.Item]]]",
items: Dict[nodes.Item, None],
argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]],
items_by_argkey: Dict[int, Dict[_Key, "Deque[nodes.Item]"]],
scopenum: int,
) -> "Dict[nodes.Item, None]":
) -> Dict[nodes.Item, None]:
if scopenum >= scopenum_function or len(items) < 3:
return items
ignore = set() # type: Set[Optional[_Key]]
ignore: Set[Optional[_Key]] = set()
items_deque = deque(items)
items_done = order_preserving_dict() # type: Dict[nodes.Item, None]
items_done: Dict[nodes.Item, None] = {}
scoped_items_by_argkey = items_by_argkey[scopenum]
scoped_argkeys_cache = argkeys_cache[scopenum]
while items_deque:
no_argkey_group = order_preserving_dict() # type: Dict[nodes.Item, None]
no_argkey_group: Dict[nodes.Item, None] = {}
slicing_argkey = None
while items_deque:
item = items_deque.popleft()
if item in items_done or item in no_argkey_group:
continue
argkeys = order_preserving_dict.fromkeys(
argkeys = dict.fromkeys(
(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None
)
if not argkeys:
@@ -339,9 +342,22 @@ def reorder_items_atscope(
return items_done
def _fillfuncargs(function: "Function") -> None:
"""Fill missing fixtures for a test function, old public API (deprecated)."""
warnings.warn(FILLFUNCARGS.format(name="pytest._fillfuncargs()"), stacklevel=2)
_fill_fixtures_impl(function)
def fillfixtures(function: "Function") -> None:
"""Fill missing funcargs for a test function."""
warnings.warn(FILLFUNCARGS, stacklevel=2)
"""Fill missing fixtures for a test function (deprecated)."""
warnings.warn(
FILLFUNCARGS.format(name="_pytest.fixtures.fillfixtures()"), stacklevel=2
)
_fill_fixtures_impl(function)
def _fill_fixtures_impl(function: "Function") -> None:
"""Internal implementation to fill fixtures on the given function object."""
try:
request = function._request
except AttributeError:
@@ -352,7 +368,7 @@ def fillfixtures(function: "Function") -> None:
assert function.parent is not None
fi = fm.getfixtureinfo(function.parent, function.obj, None)
function._fixtureinfo = fi
request = function._request = FixtureRequest(function)
request = function._request = FixtureRequest(function, _ispytest=True)
request._fillfixtures()
# Prune out funcargs for jstests.
newfuncargs = {}
@@ -389,7 +405,7 @@ class FuncFixtureInfo:
tree. In this way the dependency tree can get pruned, and the closure
of argnames may get reduced.
"""
closure = set() # type: Set[str]
closure: Set[str] = set()
working_set = set(self.initialnames)
while working_set:
argname = working_set.pop()
@@ -414,19 +430,18 @@ class FixtureRequest:
indirectly.
"""
def __init__(self, pyfuncitem) -> None:
def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._pyfuncitem = pyfuncitem
#: Fixture for which this request is being performed.
self.fixturename = None # type: Optional[str]
self.fixturename: Optional[str] = None
#: Scope string, one of "function", "class", "module", "session".
self.scope = "function" # type: _Scope
self._fixture_defs = {} # type: Dict[str, FixtureDef[Any]]
fixtureinfo = pyfuncitem._fixtureinfo # type: FuncFixtureInfo
self.scope: _Scope = "function"
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
self._arg2index = {} # type: Dict[str, int]
self._fixturemanager = (
pyfuncitem.session._fixturemanager
) # type: FixtureManager
self._arg2index: Dict[str, int] = {}
self._fixturemanager: FixtureManager = (pyfuncitem.session._fixturemanager)
@property
def fixturenames(self) -> List[str]:
@@ -462,14 +477,14 @@ class FixtureRequest:
@property
def config(self) -> Config:
"""The pytest config object associated with this request."""
return self._pyfuncitem.config # type: ignore[no-any-return] # noqa: F723
return self._pyfuncitem.config # type: ignore[no-any-return]
@property
def function(self):
"""Test function object if the request has a per-function scope."""
if self.scope != "function":
raise AttributeError(
"function not available in {}-scoped context".format(self.scope)
f"function not available in {self.scope}-scoped context"
)
return self._pyfuncitem.obj
@@ -477,9 +492,7 @@ class FixtureRequest:
def cls(self):
"""Class (can be None) where the test function was collected."""
if self.scope not in ("class", "function"):
raise AttributeError(
"cls not available in {}-scoped context".format(self.scope)
)
raise AttributeError(f"cls not available in {self.scope}-scoped context")
clscol = self._pyfuncitem.getparent(_pytest.python.Class)
if clscol:
return clscol.obj
@@ -498,18 +511,14 @@ class FixtureRequest:
def module(self):
"""Python module object where the test function was collected."""
if self.scope not in ("function", "class", "module"):
raise AttributeError(
"module not available in {}-scoped context".format(self.scope)
)
raise AttributeError(f"module not available in {self.scope}-scoped context")
return self._pyfuncitem.getparent(_pytest.python.Module).obj
@property
def fspath(self) -> py.path.local:
"""The file system path of the test module which collected this test."""
if self.scope not in ("function", "class", "module", "package"):
raise AttributeError(
"module not available in {}-scoped context".format(self.scope)
)
raise AttributeError(f"module not available in {self.scope}-scoped context")
# TODO: Remove ignore once _pyfuncitem is properly typed.
return self._pyfuncitem.fspath # type: ignore
@@ -519,9 +528,9 @@ class FixtureRequest:
return self.node.keywords
@property
def session(self):
def session(self) -> "Session":
"""Pytest session object."""
return self._pyfuncitem.session
return self._pyfuncitem.session # type: ignore[no-any-return]
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called after the last test
@@ -535,7 +544,7 @@ class FixtureRequest:
finalizer=finalizer, colitem=colitem
)
def applymarker(self, marker) -> None:
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
"""Apply a marker to a single test function invocation.
This method is useful if you don't want to have a keyword/marker
@@ -584,7 +593,7 @@ class FixtureRequest:
except FixtureLookupError:
if argname == "request":
cached_result = (self, [0], None)
scope = "function" # type: _Scope
scope: _Scope = "function"
return PseudoFixtureDef(cached_result, scope)
raise
# Remove indent to prevent the python3 exception
@@ -595,7 +604,7 @@ class FixtureRequest:
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
current = self
values = [] # type: List[FixtureDef[Any]]
values: List[FixtureDef[Any]] = []
while 1:
fixturedef = getattr(current, "_fixturedef", None)
if fixturedef is None:
@@ -667,7 +676,9 @@ class FixtureRequest:
if paramscopenum is not None:
scope = scopes[paramscopenum]
subrequest = SubRequest(self, scope, param, param_index, fixturedef)
subrequest = SubRequest(
self, scope, param, param_index, fixturedef, _ispytest=True
)
# Check if a higher-level scoped fixture accesses a lower level one.
subrequest._check_scope(argname, self.scope, scope)
@@ -685,7 +696,9 @@ class FixtureRequest:
functools.partial(fixturedef.finish, request=subrequest), subrequest.node
)
def _check_scope(self, argname, invoking_scope: "_Scope", requested_scope) -> None:
def _check_scope(
self, argname: str, invoking_scope: "_Scope", requested_scope: "_Scope",
) -> None:
if argname == "request":
return
if scopemismatch(invoking_scope, requested_scope):
@@ -709,11 +722,11 @@ class FixtureRequest:
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
return lines
def _getscopeitem(self, scope):
def _getscopeitem(self, scope: "_Scope") -> Union[nodes.Item, nodes.Collector]:
if scope == "function":
# This might also be a non-function Item despite its attribute name.
return self._pyfuncitem
if scope == "package":
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope == "package":
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on FixtureRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
@@ -742,7 +755,10 @@ class SubRequest(FixtureRequest):
param,
param_index: int,
fixturedef: "FixtureDef[object]",
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self._parent_request = request
self.fixturename = fixturedef.argname
if param is not NOTSET:
@@ -757,9 +773,11 @@ class SubRequest(FixtureRequest):
self._fixturemanager = request._fixturemanager
def __repr__(self) -> str:
return "<SubRequest {!r} for {!r}>".format(self.fixturename, self._pyfuncitem)
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called after the last test
within the requesting test context finished execution."""
self._fixturedef.addfinalizer(finalizer)
def _schedule_finalizers(
@@ -775,7 +793,7 @@ class SubRequest(FixtureRequest):
super()._schedule_finalizers(fixturedef, subrequest)
scopes = ["session", "package", "module", "class", "function"] # type: List[_Scope]
scopes: List["_Scope"] = ["session", "package", "module", "class", "function"]
scopenum_function = scopes.index("function")
@@ -786,13 +804,13 @@ def scopemismatch(currentscope: "_Scope", newscope: "_Scope") -> bool:
def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int:
"""Look up the index of ``scope`` and raise a descriptive value error
if not defined."""
strscopes = scopes # type: Sequence[str]
strscopes: Sequence[str] = scopes
try:
return strscopes.index(scope)
except ValueError:
fail(
"{} {}got an unexpected scope value '{}'".format(
descr, "from {} ".format(where) if where else "", scope
descr, f"from {where} " if where else "", scope
),
pytrace=False,
)
@@ -811,7 +829,7 @@ class FixtureLookupError(LookupError):
self.msg = msg
def formatrepr(self) -> "FixtureLookupErrorRepr":
tblines = [] # type: List[str]
tblines: List[str] = []
addline = tblines.append
stack = [self.request._pyfuncitem.obj]
stack.extend(map(lambda x: x.func, self.fixturestack))
@@ -848,7 +866,7 @@ class FixtureLookupError(LookupError):
self.argname
)
else:
msg = "fixture '{}' not found".format(self.argname)
msg = f"fixture '{self.argname}' not found"
msg += "\n available fixtures: {}".format(", ".join(sorted(available)))
msg += "\n use 'pytest --fixtures [testpath]' for help on them."
@@ -882,8 +900,7 @@ class FixtureLookupErrorRepr(TerminalRepr):
)
for line in lines[1:]:
tw.line(
"{} {}".format(FormattedExcinfo.flow_marker, line.strip()),
red=True,
f"{FormattedExcinfo.flow_marker} {line.strip()}", red=True,
)
tw.line()
tw.line("%s:%d" % (self.filename, self.firstlineno + 1))
@@ -907,9 +924,7 @@ def call_fixture_func(
try:
fixture_result = next(generator)
except StopIteration:
raise ValueError(
"{} did not yield a value".format(request.fixturename)
) from None
raise ValueError(f"{request.fixturename} did not yield a value") from None
finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
request.addfinalizer(finalizer)
else:
@@ -962,7 +977,7 @@ class FixtureDef(Generic[_FixtureValue]):
def __init__(
self,
fixturemanager: "FixtureManager",
baseid,
baseid: Optional[str],
argname: str,
func: "_FixtureFunc[_FixtureValue]",
scope: "Union[_Scope, Callable[[str, Config], _Scope]]",
@@ -987,18 +1002,18 @@ class FixtureDef(Generic[_FixtureValue]):
self.scopenum = scope2index(
# TODO: Check if the `or` here is really necessary.
scope_ or "function", # type: ignore[unreachable]
descr="Fixture '{}'".format(func.__name__),
descr=f"Fixture '{func.__name__}'",
where=baseid,
)
self.scope = scope_
self.params = params # type: Optional[Sequence[object]]
self.argnames = getfuncargnames(
self.params: Optional[Sequence[object]] = params
self.argnames: Tuple[str, ...] = getfuncargnames(
func, name=argname, is_method=unittest
) # type: Tuple[str, ...]
)
self.unittest = unittest
self.ids = ids
self.cached_result = None # type: Optional[_FixtureCachedResult[_FixtureValue]]
self._finalizers = [] # type: List[Callable[[], object]]
self.cached_result: Optional[_FixtureCachedResult[_FixtureValue]] = None
self._finalizers: List[Callable[[], object]] = []
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
self._finalizers.append(finalizer)
@@ -1144,7 +1159,9 @@ def _params_converter(
return tuple(params) if params is not None else None
def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
def wrap_function_to_error_out_if_called_directly(
function: _FixtureFunction, fixture_marker: "FixtureFunctionMarker",
) -> _FixtureFunction:
"""Wrap the given fixture function so we can raise an error about it being called directly,
instead of used as an argument in a test function."""
message = (
@@ -1162,7 +1179,7 @@ def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
# further than this point and lose useful wrappings like @mock.patch (#3774).
result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]
return result
return cast(_FixtureFunction, result)
@final
@@ -1220,13 +1237,13 @@ def fixture(
Callable[[Any], Optional[object]],
]
] = ...,
name: Optional[str] = ...
name: Optional[str] = ...,
) -> _FixtureFunction:
...
@overload # noqa: F811
def fixture( # noqa: F811
@overload
def fixture(
fixture_function: None = ...,
*,
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = ...,
@@ -1238,12 +1255,12 @@ def fixture( # noqa: F811
Callable[[Any], Optional[object]],
]
] = ...,
name: Optional[str] = None
name: Optional[str] = None,
) -> FixtureFunctionMarker:
...
def fixture( # noqa: F811
def fixture(
fixture_function: Optional[_FixtureFunction] = None,
*,
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function",
@@ -1255,7 +1272,7 @@ def fixture( # noqa: F811
Callable[[Any], Optional[object]],
]
] = None,
name: Optional[str] = None
name: Optional[str] = None,
) -> Union[FixtureFunctionMarker, _FixtureFunction]:
"""Decorator to mark a fixture factory function.
@@ -1325,13 +1342,14 @@ def yield_fixture(
params=None,
autouse=False,
ids=None,
name=None
name=None,
):
"""(Return a) decorator to mark a yield-fixture factory function.
.. deprecated:: 3.0
Use :py:func:`pytest.fixture` directly instead.
"""
warnings.warn(YIELD_FIXTURE, stacklevel=2)
return fixture(
fixture_function,
*args,
@@ -1402,15 +1420,16 @@ class FixtureManager:
def __init__(self, session: "Session") -> None:
self.session = session
self.config = session.config # type: Config
self._arg2fixturedefs = {} # type: Dict[str, List[FixtureDef[Any]]]
self._holderobjseen = set() # type: Set[object]
self._nodeid_and_autousenames = [
("", self.config.getini("usefixtures"))
] # type: List[Tuple[str, List[str]]]
self.config: Config = session.config
self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {}
self._holderobjseen: Set[object] = set()
# A mapping from a nodeid to a list of autouse fixtures it defines.
self._nodeid_autousenames: Dict[str, List[str]] = {
"": self.config.getini("usefixtures"),
}
session.config.pluginmanager.register(self, "funcmanage")
def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]:
def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]:
"""Return all direct parametrization arguments of a node, so we don't
mistake them for fixtures.
@@ -1419,7 +1438,7 @@ class FixtureManager:
These things are done later as well when dealing with parametrization
so this could be improved.
"""
parametrize_argnames = [] # type: List[str]
parametrize_argnames: List[str] = []
for marker in node.iter_markers(name="parametrize"):
if not marker.kwargs.get("indirect", False):
p_argnames, _ = ParameterSet._parse_parametrize_args(
@@ -1430,7 +1449,7 @@ class FixtureManager:
return parametrize_argnames
def getfixtureinfo(
self, node: "nodes.Node", func, cls, funcargs: bool = True
self, node: nodes.Node, func, cls, funcargs: bool = True
) -> FuncFixtureInfo:
if funcargs and not getattr(node, "nofuncargs", False):
argnames = getfuncargnames(func, name=node.name, cls=cls)
@@ -1454,8 +1473,6 @@ class FixtureManager:
except AttributeError:
pass
else:
from _pytest import nodes
# Construct the base nodeid which is later used to check
# what fixtures are visible for particular tests (as denoted
# by their test id).
@@ -1471,21 +1488,18 @@ class FixtureManager:
self.parsefactories(plugin, nodeid)
def _getautousenames(self, nodeid: str) -> List[str]:
"""Return a list of fixture names to be used."""
autousenames = [] # type: List[str]
for baseid, basenames in self._nodeid_and_autousenames:
if nodeid.startswith(baseid):
if baseid:
i = len(baseid)
nextchar = nodeid[i : i + 1]
if nextchar and nextchar not in ":/":
continue
autousenames.extend(basenames)
return autousenames
def _getautousenames(self, nodeid: str) -> Iterator[str]:
"""Return the names of autouse fixtures applicable to nodeid."""
for parentnodeid in nodes.iterparentnodeids(nodeid):
basenames = self._nodeid_autousenames.get(parentnodeid)
if basenames:
yield from basenames
def getfixtureclosure(
self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = ()
self,
fixturenames: Tuple[str, ...],
parentnode: nodes.Node,
ignore_args: Sequence[str] = (),
) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
# Collect the closure of all fixtures, starting with the given
# fixturenames as the initial set. As we have to visit all
@@ -1495,7 +1509,7 @@ class FixtureManager:
# (discovering matching fixtures for a given name/node is expensive).
parentid = parentnode.nodeid
fixturenames_closure = self._getautousenames(parentid)
fixturenames_closure = list(self._getautousenames(parentid))
def merge(otherlist: Iterable[str]) -> None:
for arg in otherlist:
@@ -1509,7 +1523,7 @@ class FixtureManager:
# need to return it as well, so save this.
initialnames = tuple(fixturenames_closure)
arg2fixturedefs = {} # type: Dict[str, Sequence[FixtureDef[Any]]]
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
lastlen = -1
while lastlen != len(fixturenames_closure):
lastlen = len(fixturenames_closure)
@@ -1579,7 +1593,7 @@ class FixtureManager:
# Try next super fixture, if any.
def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None:
def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None:
# Separate parametrized setups.
items[:] = reorder_items(items)
@@ -1640,7 +1654,7 @@ class FixtureManager:
autousenames.append(name)
if autousenames:
self._nodeid_and_autousenames.append((nodeid or "", autousenames))
self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames)
def getfixturedefs(
self, argname: str, nodeid: str
@@ -1660,8 +1674,7 @@ class FixtureManager:
def _matchfactories(
self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
) -> Iterator[FixtureDef[Any]]:
from _pytest import nodes
parentnodeids = set(nodes.iterparentnodeids(nodeid))
for fixturedef in fixturedefs:
if nodes.ischildnode(fixturedef.baseid, nodeid):
if fixturedef.baseid in parentnodeids:
yield fixturedef

View File

@@ -97,7 +97,7 @@ def pytest_addoption(parser: Parser) -> None:
@pytest.hookimpl(hookwrapper=True)
def pytest_cmdline_parse():
outcome = yield
config = outcome.get_result() # type: Config
config: Config = outcome.get_result()
if config.option.debug:
path = os.path.abspath("pytestdebug.log")
debugfile = open(path, "w")
@@ -137,7 +137,7 @@ def showversion(config: Config) -> None:
for line in plugininfo:
sys.stderr.write(line + "\n")
else:
sys.stderr.write("pytest {}\n".format(pytest.__version__))
sys.stderr.write(f"pytest {pytest.__version__}\n")
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
@@ -172,8 +172,8 @@ def showhelp(config: Config) -> None:
if type is None:
type = "string"
if help is None:
raise TypeError("help argument cannot be None for {}".format(name))
spec = "{} ({}):".format(name, type)
raise TypeError(f"help argument cannot be None for {name}")
spec = f"{name} ({type}):"
tw.write(" %s" % spec)
spec_len = len(spec)
if spec_len > (indent_len - 3):
@@ -208,7 +208,7 @@ def showhelp(config: Config) -> None:
("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals"),
]
for name, help in vars:
tw.line(" {:<24} {}".format(name, help))
tw.line(f" {name:<24} {help}")
tw.line()
tw.line()
@@ -235,7 +235,7 @@ def getpluginversioninfo(config: Config) -> List[str]:
lines.append("setuptools registered plugins:")
for plugin, dist in plugininfo:
loc = getattr(plugin, "__file__", repr(plugin))
content = "{}-{} at {}".format(dist.project_name, dist.version, loc)
content = f"{dist.project_name}-{dist.version} at {loc}"
lines.append(" " + content)
return lines
@@ -243,9 +243,7 @@ def getpluginversioninfo(config: Config) -> List[str]:
def pytest_report_header(config: Config) -> List[str]:
lines = []
if config.option.debug or config.option.traceconfig:
lines.append(
"using: pytest-{} pylib-{}".format(pytest.__version__, py.__version__)
)
lines.append(f"using: pytest-{pytest.__version__} pylib-{py.__version__}")
verinfo = getpluginversioninfo(config)
if verinfo:
@@ -259,5 +257,5 @@ def pytest_report_header(config: Config) -> List[str]:
r = plugin.__file__
else:
r = repr(plugin)
lines.append(" {:<20}: {}".format(name, r))
lines.append(f" {name:<20}: {r}")
return lines

View File

@@ -7,12 +7,12 @@ from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import py.path
from pluggy import HookspecMarker
from _pytest.compat import TYPE_CHECKING
from _pytest.deprecated import WARNING_CAPTURED_HOOK
if TYPE_CHECKING:
@@ -446,7 +446,7 @@ def pytest_runtest_logstart(
See :func:`pytest_runtest_protocol` for a description of the runtest protocol.
:param str nodeid: Full node ID of the item.
:param location: A triple of ``(filename, lineno, testname)``.
:param location: A tuple of ``(filename, lineno, testname)``.
"""
@@ -458,7 +458,7 @@ def pytest_runtest_logfinish(
See :func:`pytest_runtest_protocol` for a description of the runtest protocol.
:param str nodeid: Full node ID of the item.
:param location: A triple of ``(filename, lineno, testname)``.
:param location: A tuple of ``(filename, lineno, testname)``.
"""
@@ -808,6 +808,27 @@ def pytest_warning_recorded(
"""
# -------------------------------------------------------------------------
# Hooks for influencing skipping
# -------------------------------------------------------------------------
def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]:
"""Called when constructing the globals dictionary used for
evaluating string conditions in xfail/skipif markers.
This is useful when the condition for a marker requires
objects that are expensive or impossible to obtain during
collection time, which is required by normal boolean
conditions.
.. versionadded:: 6.2
:param _pytest.config.Config config: The pytest config object.
:returns: A dictionary of additional globals to add.
"""
# -------------------------------------------------------------------------
# error handling and internal debugging hooks
# -------------------------------------------------------------------------

View File

@@ -93,9 +93,9 @@ class _NodeReporter:
self.add_stats = self.xml.add_stats
self.family = self.xml.family
self.duration = 0
self.properties = [] # type: List[Tuple[str, str]]
self.nodes = [] # type: List[ET.Element]
self.attrs = {} # type: Dict[str, str]
self.properties: List[Tuple[str, str]] = []
self.nodes: List[ET.Element] = []
self.attrs: Dict[str, str] = {}
def append(self, node: ET.Element) -> None:
self.xml.add_stats(node.tag)
@@ -122,11 +122,11 @@ class _NodeReporter:
classnames = names[:-1]
if self.xml.prefix:
classnames.insert(0, self.xml.prefix)
attrs = {
attrs: Dict[str, str] = {
"classname": ".".join(classnames),
"name": bin_xml_escape(names[-1]),
"file": testreport.location[0],
} # type: Dict[str, str]
}
if testreport.location[1] is not None:
attrs["line"] = str(testreport.location[1])
if hasattr(testreport, "url"):
@@ -199,9 +199,9 @@ class _NodeReporter:
self._add_simple("skipped", "xfail-marked test passes unexpectedly")
else:
assert report.longrepr is not None
reprcrash = getattr(
reprcrash: Optional[ReprFileLocation] = getattr(
report.longrepr, "reprcrash", None
) # type: Optional[ReprFileLocation]
)
if reprcrash is not None:
message = reprcrash.message
else:
@@ -219,18 +219,18 @@ class _NodeReporter:
def append_error(self, report: TestReport) -> None:
assert report.longrepr is not None
reprcrash = getattr(
reprcrash: Optional[ReprFileLocation] = getattr(
report.longrepr, "reprcrash", None
) # type: Optional[ReprFileLocation]
)
if reprcrash is not None:
reason = reprcrash.message
else:
reason = str(report.longrepr)
if report.when == "teardown":
msg = 'failed on teardown with "{}"'.format(reason)
msg = f'failed on teardown with "{reason}"'
else:
msg = 'failed on setup with "{}"'.format(reason)
msg = f'failed on setup with "{reason}"'
self._add_simple("error", msg, str(report.longrepr))
def append_skipped(self, report: TestReport) -> None:
@@ -246,7 +246,7 @@ class _NodeReporter:
filename, lineno, skipreason = report.longrepr
if skipreason.startswith("Skipped: "):
skipreason = skipreason[9:]
details = "{}:{}: {}".format(filename, lineno, skipreason)
details = f"{filename}:{lineno}: {skipreason}"
skipped = ET.Element("skipped", type="pytest.skip", message=skipreason)
skipped.text = bin_xml_escape(details)
@@ -481,17 +481,17 @@ class LogXML:
self.log_passing_tests = log_passing_tests
self.report_duration = report_duration
self.family = family
self.stats = dict.fromkeys(
self.stats: Dict[str, int] = dict.fromkeys(
["error", "passed", "failure", "skipped"], 0
) # type: Dict[str, int]
self.node_reporters = (
{}
) # type: Dict[Tuple[Union[str, TestReport], object], _NodeReporter]
self.node_reporters_ordered = [] # type: List[_NodeReporter]
self.global_properties = [] # type: List[Tuple[str, str]]
)
self.node_reporters: Dict[
Tuple[Union[str, TestReport], object], _NodeReporter
] = ({})
self.node_reporters_ordered: List[_NodeReporter] = []
self.global_properties: List[Tuple[str, str]] = []
# List of reports that failed on call but teardown is pending.
self.open_reports = [] # type: List[TestReport]
self.open_reports: List[TestReport] = []
self.cnt_double_fail_tests = 0
# Replaces convenience family with real family.
@@ -507,7 +507,7 @@ class LogXML:
reporter.finalize()
def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter:
nodeid = getattr(report, "nodeid", report) # type: Union[str, TestReport]
nodeid: Union[str, TestReport] = getattr(report, "nodeid", report)
# Local hack to handle xdist report order.
workernode = getattr(report, "node", None)
@@ -683,7 +683,7 @@ class LogXML:
logfile.close()
def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None:
terminalreporter.write_sep("-", "generated xml file: {}".format(self.logfile))
terminalreporter.write_sep("-", f"generated xml file: {self.logfile}")
def add_global_property(self, name: str, value: object) -> None:
__tracebackhide__ = True

View File

@@ -5,6 +5,7 @@ import re
import sys
from contextlib import contextmanager
from io import StringIO
from pathlib import Path
from typing import AbstractSet
from typing import Dict
from typing import Generator
@@ -15,7 +16,6 @@ from typing import Tuple
from typing import TypeVar
from typing import Union
import pytest
from _pytest import nodes
from _pytest._io import TerminalWriter
from _pytest.capture import CaptureManager
@@ -24,10 +24,13 @@ from _pytest.compat import nullcontext
from _pytest.config import _strtobool
from _pytest.config import Config
from _pytest.config import create_terminal_writer
from _pytest.config import hookimpl
from _pytest.config import UsageError
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.main import Session
from _pytest.pathlib import Path
from _pytest.store import StoreKey
from _pytest.terminal import TerminalReporter
@@ -47,7 +50,7 @@ class ColoredLevelFormatter(logging.Formatter):
"""A logging formatter which colorizes the %(levelname)..s part of the
log format passed to __init__."""
LOGLEVEL_COLOROPTS = {
LOGLEVEL_COLOROPTS: Mapping[int, AbstractSet[str]] = {
logging.CRITICAL: {"red"},
logging.ERROR: {"red", "bold"},
logging.WARNING: {"yellow"},
@@ -55,13 +58,13 @@ class ColoredLevelFormatter(logging.Formatter):
logging.INFO: {"green"},
logging.DEBUG: {"purple"},
logging.NOTSET: set(),
} # type: Mapping[int, AbstractSet[str]]
}
LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*s)")
def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._original_fmt = self._style._fmt
self._level_to_fmt_mapping = {} # type: Dict[int, str]
self._level_to_fmt_mapping: Dict[int, str] = {}
assert self._fmt is not None
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
@@ -315,12 +318,12 @@ class catching_logs:
class LogCaptureHandler(logging.StreamHandler):
"""A logging handler that stores log records and the log text."""
stream = None # type: StringIO
stream: StringIO
def __init__(self) -> None:
"""Create a new log handler."""
super().__init__(StringIO())
self.records = [] # type: List[logging.LogRecord]
self.records: List[logging.LogRecord] = []
def emit(self, record: logging.LogRecord) -> None:
"""Keep the log records in a list in addition to the log text."""
@@ -344,11 +347,12 @@ class LogCaptureHandler(logging.StreamHandler):
class LogCaptureFixture:
"""Provides access and control of log capturing."""
def __init__(self, item: nodes.Node) -> None:
def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._item = item
self._initial_handler_level = None # type: Optional[int]
self._initial_handler_level: Optional[int] = None
# Dict of log name -> log level.
self._initial_logger_levels = {} # type: Dict[Optional[str], int]
self._initial_logger_levels: Dict[Optional[str], int] = {}
def _finalize(self) -> None:
"""Finalize the fixture.
@@ -468,7 +472,7 @@ class LogCaptureFixture:
self.handler.setLevel(handler_orig_level)
@pytest.fixture
@fixture
def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
"""Access and control log capturing.
@@ -480,7 +484,7 @@ def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
* caplog.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string
"""
result = LogCaptureFixture(request.node)
result = LogCaptureFixture(request.node, _ispytest=True)
yield result
result._finalize()
@@ -501,7 +505,7 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i
return int(getattr(logging, log_level, log_level))
except ValueError as e:
# Python logging does not recognise this as a logging level
raise pytest.UsageError(
raise UsageError(
"'{}' is not recognized as a logging level name for "
"'{}'. Please consider passing the "
"logging level num instead.".format(log_level, setting_name)
@@ -509,7 +513,7 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i
# run after terminalreporter/capturemanager are configured
@pytest.hookimpl(trylast=True)
@hookimpl(trylast=True)
def pytest_configure(config: Config) -> None:
config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
@@ -564,9 +568,9 @@ class LoggingPlugin:
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
capture_manager = config.pluginmanager.get_plugin("capturemanager")
# if capturemanager plugin is disabled, live logging still works.
self.log_cli_handler = _LiveLoggingStreamHandler(
terminal_reporter, capture_manager
) # type: Union[_LiveLoggingStreamHandler, _LiveLoggingNullHandler]
self.log_cli_handler: Union[
_LiveLoggingStreamHandler, _LiveLoggingNullHandler
] = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
else:
self.log_cli_handler = _LiveLoggingNullHandler()
log_cli_formatter = self._create_formatter(
@@ -582,9 +586,9 @@ class LoggingPlugin:
if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
log_format
):
formatter = ColoredLevelFormatter(
formatter: logging.Formatter = ColoredLevelFormatter(
create_terminal_writer(self._config), log_format, log_date_format
) # type: logging.Formatter
)
else:
formatter = logging.Formatter(log_format, log_date_format)
@@ -639,7 +643,7 @@ class LoggingPlugin:
return True
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@hookimpl(hookwrapper=True, tryfirst=True)
def pytest_sessionstart(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("sessionstart")
@@ -647,7 +651,7 @@ class LoggingPlugin:
with catching_logs(self.log_file_handler, level=self.log_file_level):
yield
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@hookimpl(hookwrapper=True, tryfirst=True)
def pytest_collection(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("collection")
@@ -655,7 +659,7 @@ class LoggingPlugin:
with catching_logs(self.log_file_handler, level=self.log_file_level):
yield
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
if session.config.option.collectonly:
yield
@@ -669,12 +673,12 @@ class LoggingPlugin:
with catching_logs(self.log_file_handler, level=self.log_file_level):
yield # Run all the tests.
@pytest.hookimpl
@hookimpl
def pytest_runtest_logstart(self) -> None:
self.log_cli_handler.reset()
self.log_cli_handler.set_when("start")
@pytest.hookimpl
@hookimpl
def pytest_runtest_logreport(self) -> None:
self.log_cli_handler.set_when("logreport")
@@ -695,21 +699,21 @@ class LoggingPlugin:
log = report_handler.stream.getvalue().strip()
item.add_report_section(when, "log", log)
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("setup")
empty = {} # type: Dict[str, List[logging.LogRecord]]
empty: Dict[str, List[logging.LogRecord]] = {}
item._store[caplog_records_key] = empty
yield from self._runtest_for(item, "setup")
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("call")
yield from self._runtest_for(item, "call")
@pytest.hookimpl(hookwrapper=True)
@hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("teardown")
@@ -717,11 +721,11 @@ class LoggingPlugin:
del item._store[caplog_records_key]
del item._store[caplog_handler_key]
@pytest.hookimpl
@hookimpl
def pytest_runtest_logfinish(self) -> None:
self.log_cli_handler.set_when("finish")
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
@hookimpl(hookwrapper=True, tryfirst=True)
def pytest_sessionfinish(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("sessionfinish")
@@ -729,7 +733,7 @@ class LoggingPlugin:
with catching_logs(self.log_file_handler, level=self.log_file_level):
yield
@pytest.hookimpl
@hookimpl
def pytest_unconfigure(self) -> None:
# Close the FileHandler explicitly.
# (logging.shutdown might have lost the weakref?!)
@@ -755,7 +759,7 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
# Officially stream needs to be a IO[str], but TerminalReporter
# isn't. So force it.
stream = None # type: TerminalReporter # type: ignore
stream: TerminalReporter = None # type: ignore
def __init__(
self,

View File

@@ -5,15 +5,19 @@ import functools
import importlib
import os
import sys
from pathlib import Path
from typing import Callable
from typing import Dict
from typing import FrozenSet
from typing import Iterator
from typing import List
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import attr
@@ -22,8 +26,6 @@ import py
import _pytest._code
from _pytest import nodes
from _pytest.compat import final
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config import directory_arg
from _pytest.config import ExitCode
@@ -35,7 +37,6 @@ from _pytest.fixtures import FixtureManager
from _pytest.outcomes import exit
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import Path
from _pytest.pathlib import visit
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
@@ -44,7 +45,6 @@ from _pytest.runner import SetupState
if TYPE_CHECKING:
from typing import Type
from typing_extensions import Literal
@@ -53,7 +53,17 @@ def pytest_addoption(parser: Parser) -> None:
"norecursedirs",
"directory patterns to avoid for recursion",
type="args",
default=[".*", "build", "dist", "CVS", "_darcs", "{arch}", "*.egg", "venv"],
default=[
"*.egg",
".*",
"_darcs",
"build",
"CVS",
"dist",
"node_modules",
"venv",
"{arch}",
],
)
parser.addini(
"testpaths",
@@ -101,10 +111,12 @@ def pytest_addoption(parser: Parser) -> None:
)
group._addoption(
"--strict-markers",
"--strict",
action="store_true",
help="markers not registered in the `markers` section of the configuration file raise errors.",
)
group._addoption(
"--strict", action="store_true", help="(deprecated) alias to --strict-markers.",
)
group._addoption(
"-c",
metavar="file",
@@ -262,14 +274,12 @@ def wrap_session(
session.exitstatus = ExitCode.TESTS_FAILED
except (KeyboardInterrupt, exit.Exception):
excinfo = _pytest._code.ExceptionInfo.from_current()
exitstatus = ExitCode.INTERRUPTED # type: Union[int, ExitCode]
exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED
if isinstance(excinfo.value, exit.Exception):
if excinfo.value.returncode is not None:
exitstatus = excinfo.value.returncode
if initstate < 2:
sys.stderr.write(
"{}: {}\n".format(excinfo.typename, excinfo.value.msg)
)
sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n")
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
session.exitstatus = exitstatus
except BaseException:
@@ -441,10 +451,10 @@ class Session(nodes.FSCollector):
Interrupted = Interrupted
Failed = Failed
# Set on the session by runner.pytest_sessionstart.
_setupstate = None # type: SetupState
_setupstate: SetupState
# Set on the session by fixtures.pytest_sessionstart.
_fixturemanager = None # type: FixtureManager
exitstatus = None # type: Union[int, ExitCode]
_fixturemanager: FixtureManager
exitstatus: Union[int, ExitCode]
def __init__(self, config: Config) -> None:
super().__init__(
@@ -452,21 +462,19 @@ class Session(nodes.FSCollector):
)
self.testsfailed = 0
self.testscollected = 0
self.shouldstop = False # type: Union[bool, str]
self.shouldfail = False # type: Union[bool, str]
self.shouldstop: Union[bool, str] = False
self.shouldfail: Union[bool, str] = False
self.trace = config.trace.root.get("collection")
self.startdir = config.invocation_dir
self._initialpaths = frozenset() # type: FrozenSet[py.path.local]
self._initialpaths: FrozenSet[py.path.local] = frozenset()
self._bestrelpathcache = _bestrelpath_cache(
config.rootpath
) # type: Dict[Path, str]
self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath)
self.config.pluginmanager.register(self, name="session")
@classmethod
def from_config(cls, config: Config) -> "Session":
session = cls._create(config) # type: Session
session: Session = cls._create(config)
return session
def __repr__(self) -> str:
@@ -562,13 +570,13 @@ class Session(nodes.FSCollector):
) -> Sequence[nodes.Item]:
...
@overload # noqa: F811
def perform_collect( # noqa: F811
@overload
def perform_collect(
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
...
def perform_collect( # noqa: F811
def perform_collect(
self, args: Optional[Sequence[str]] = None, genitems: bool = True
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
"""Perform the collection phase for this session.
@@ -591,15 +599,15 @@ class Session(nodes.FSCollector):
self.trace("perform_collect", self, args)
self.trace.root.indent += 1
self._notfound = [] # type: List[Tuple[str, Sequence[nodes.Collector]]]
self._initial_parts = [] # type: List[Tuple[py.path.local, List[str]]]
self.items = [] # type: List[nodes.Item]
self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = []
self._initial_parts: List[Tuple[py.path.local, List[str]]] = []
self.items: List[nodes.Item] = []
hook = self.config.hook
items = self.items # type: Sequence[Union[nodes.Item, nodes.Collector]]
items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items
try:
initialpaths = [] # type: List[py.path.local]
initialpaths: List[py.path.local] = []
for arg in args:
fspath, parts = resolve_collection_argument(
self.config.invocation_params.dir,
@@ -615,8 +623,8 @@ class Session(nodes.FSCollector):
if self._notfound:
errors = []
for arg, cols in self._notfound:
line = "(no name {!r} in any of {!r})".format(arg, cols)
errors.append("not found: {}\n{}".format(arg, line))
line = f"(no name {arg!r} in any of {cols!r})"
errors.append(f"not found: {arg}\n{line}")
raise UsageError(*errors)
if not genitems:
items = rep.result
@@ -639,19 +647,17 @@ class Session(nodes.FSCollector):
from _pytest.python import Package
# Keep track of any collected nodes in here, so we don't duplicate fixtures.
node_cache1 = {} # type: Dict[py.path.local, Sequence[nodes.Collector]]
node_cache2 = (
{}
) # type: Dict[Tuple[Type[nodes.Collector], py.path.local], nodes.Collector]
node_cache1: Dict[py.path.local, Sequence[nodes.Collector]] = {}
node_cache2: Dict[
Tuple[Type[nodes.Collector], py.path.local], nodes.Collector
] = ({})
# Keep track of any collected collectors in matchnodes paths, so they
# are not collected more than once.
matchnodes_cache = (
{}
) # type: Dict[Tuple[Type[nodes.Collector], str], CollectReport]
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = ({})
# Dirnames of pkgs with dunder-init files.
pkg_roots = {} # type: Dict[str, Package]
pkg_roots: Dict[str, Package] = {}
for argpath, names in self._initial_parts:
self.trace("processing argument", (argpath, names))
@@ -680,7 +686,7 @@ class Session(nodes.FSCollector):
if argpath.check(dir=1):
assert not names, "invalid arg {!r}".format((argpath, names))
seen_dirs = set() # type: Set[py.path.local]
seen_dirs: Set[py.path.local] = set()
for direntry in visit(str(argpath), self._recurse):
if not direntry.is_file():
continue
@@ -720,9 +726,9 @@ class Session(nodes.FSCollector):
node_cache1[argpath] = col
matching = []
work = [
(col, names)
] # type: List[Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]]
work: List[
Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]]
] = [(col, names)]
while work:
self.trace("matchnodes", col, names)
self.trace.root.indent += 1
@@ -769,12 +775,14 @@ class Session(nodes.FSCollector):
self._notfound.append((report_arg, col))
continue
# If __init__.py was the only file requested, then the matched node will be
# the corresponding Package, and the first yielded item will be the __init__
# Module itself, so just use that. If this special case isn't taken, then all
# the files in the package will be yielded.
if argpath.basename == "__init__.py":
assert isinstance(matching[0], nodes.Collector)
# If __init__.py was the only file requested, then the matched
# node will be the corresponding Package (by default), and the
# first yielded item will be the __init__ Module itself, so
# just use that. If this special case isn't taken, then all the
# files in the package will be yielded.
if argpath.basename == "__init__.py" and isinstance(
matching[0], Package
):
try:
yield next(iter(matching[0].collect()))
except StopIteration:

View File

@@ -1,9 +1,10 @@
"""Generic mechanism for marking and selecting python functions."""
import typing
import warnings
from typing import AbstractSet
from typing import Collection
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
import attr
@@ -17,7 +18,6 @@ from .structures import MARK_GEN
from .structures import MarkDecorator
from .structures import MarkGenerator
from .structures import ParameterSet
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
@@ -46,8 +46,8 @@ old_mark_config_key = StoreKey[Optional[Config]]()
def param(
*values: object,
marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (),
id: Optional[str] = None
marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),
id: Optional[str] = None,
) -> ParameterSet:
"""Specify a parameter in `pytest.mark.parametrize`_ calls or
:ref:`parametrized fixtures <fixture-parametrize-marks>`.
@@ -201,7 +201,7 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
expression = Expression.compile(keywordexpr)
except ParseError as e:
raise UsageError(
"Wrong expression passed to '-k': {}: {}".format(keywordexpr, e)
f"Wrong expression passed to '-k': {keywordexpr}: {e}"
) from None
remaining = []
@@ -245,9 +245,7 @@ def deselect_by_mark(items: "List[Item]", config: Config) -> None:
try:
expression = Expression.compile(matchexpr)
except ParseError as e:
raise UsageError(
"Wrong expression passed to '-m': {}: {}".format(matchexpr, e)
) from None
raise UsageError(f"Wrong expression passed to '-m': {matchexpr}: {e}") from None
remaining = []
deselected = []

View File

@@ -23,11 +23,10 @@ from typing import Iterator
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import TYPE_CHECKING
import attr
from _pytest.compat import TYPE_CHECKING
if TYPE_CHECKING:
from typing import NoReturn
@@ -67,7 +66,7 @@ class ParseError(Exception):
self.message = message
def __str__(self) -> str:
return "at column {}: {}".format(self.column, self.message)
return f"at column {self.column}: {self.message}"
class Scanner:
@@ -134,7 +133,7 @@ IDENT_PREFIX = "$"
def expression(s: Scanner) -> ast.Expression:
if s.accept(TokenType.EOF):
ret = ast.NameConstant(False) # type: ast.expr
ret: ast.expr = ast.NameConstant(False)
else:
ret = expr(s)
s.accept(TokenType.EOF, reject=True)
@@ -204,9 +203,9 @@ class Expression:
:param input: The input expression - one line.
"""
astexpr = expression(Scanner(input))
code = compile(
code: types.CodeType = compile(
astexpr, filename="<pytest match expression>", mode="eval",
) # type: types.CodeType
)
return Expression(code)
def evaluate(self, matcher: Callable[[str], bool]) -> bool:
@@ -218,7 +217,5 @@ class Expression:
:returns: Whether the expression matches or not.
"""
ret = eval(
self.code, {"__builtins__": {}}, MatcherAdapter(matcher)
) # type: bool
ret: bool = eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher))
return ret

View File

@@ -1,18 +1,22 @@
import collections.abc
import inspect
import typing
import warnings
from typing import Any
from typing import Callable
from typing import Collection
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Mapping
from typing import MutableMapping
from typing import NamedTuple
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
@@ -23,15 +27,11 @@ from ..compat import ascii_escaped
from ..compat import final
from ..compat import NOTSET
from ..compat import NotSetType
from ..compat import overload
from ..compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.outcomes import fail
from _pytest.warning_types import PytestUnknownMarkWarning
if TYPE_CHECKING:
from typing import Type
from ..nodes import Node
@@ -79,7 +79,7 @@ class ParameterSet(
"ParameterSet",
[
("values", Sequence[Union[object, NotSetType]]),
("marks", "typing.Collection[Union[MarkDecorator, Mark]]"),
("marks", Collection[Union["MarkDecorator", "Mark"]]),
("id", Optional[str]),
],
)
@@ -88,14 +88,13 @@ class ParameterSet(
def param(
cls,
*values: object,
marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (),
id: Optional[str] = None
marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (),
id: Optional[str] = None,
) -> "ParameterSet":
if isinstance(marks, MarkDecorator):
marks = (marks,)
else:
# TODO(py36): Change to collections.abc.Collection.
assert isinstance(marks, (collections.abc.Sequence, set))
assert isinstance(marks, collections.abc.Collection)
if id is not None:
if not isinstance(id, str):
@@ -128,7 +127,7 @@ class ParameterSet(
return cls.param(parameterset)
else:
# TODO: Refactor to fix this type-ignore. Currently the following
# type-checks but crashes:
# passes type-checking but crashes:
#
# @pytest.mark.parametrize(('x', 'y'), [1, 2])
# def test_foo(x, y): pass
@@ -139,7 +138,7 @@ class ParameterSet(
argnames: Union[str, List[str], Tuple[str, ...]],
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
*args,
**kwargs
**kwargs,
) -> Tuple[Union[List[str], Tuple[str, ...]], bool]:
if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
@@ -232,7 +231,7 @@ class Mark:
assert self.name == other.name
# Remember source of ids with parametrize Marks.
param_ids_from = None # type: Optional[Mark]
param_ids_from: Optional[Mark] = None
if self.name == "parametrize":
if other._has_param_ids():
param_ids_from = other
@@ -311,7 +310,7 @@ class MarkDecorator:
return self.name # for backward-compat (2.4.1 had this attr)
def __repr__(self) -> str:
return "<MarkDecorator {!r}>".format(self.mark)
return f"<MarkDecorator {self.mark!r}>"
def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator":
"""Return a MarkDecorator with extra arguments added.
@@ -331,13 +330,11 @@ class MarkDecorator:
def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc]
pass
@overload # noqa: F811
def __call__( # noqa: F811
self, *args: object, **kwargs: object
) -> "MarkDecorator":
@overload
def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator":
pass
def __call__(self, *args: object, **kwargs: object): # noqa: F811
def __call__(self, *args: object, **kwargs: object):
"""Call the MarkDecorator."""
if args and not kwargs:
func = args[0]
@@ -367,7 +364,7 @@ def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List
] # unpack MarkDecorator
for mark in extracted:
if not isinstance(mark, Mark):
raise TypeError("got {!r} instead of Mark".format(mark))
raise TypeError(f"got {mark!r} instead of Mark")
return [x for x in extracted if isinstance(x, Mark)]
@@ -392,8 +389,8 @@ if TYPE_CHECKING:
def __call__(self, arg: _Markable) -> _Markable:
...
@overload # noqa: F811
def __call__(self, reason: str = ...) -> "MarkDecorator": # noqa: F811
@overload
def __call__(self, reason: str = ...) -> "MarkDecorator":
...
class _SkipifMarkDecorator(MarkDecorator):
@@ -401,7 +398,7 @@ if TYPE_CHECKING:
self,
condition: Union[str, bool] = ...,
*conditions: Union[str, bool],
reason: str = ...
reason: str = ...,
) -> MarkDecorator:
...
@@ -410,17 +407,15 @@ if TYPE_CHECKING:
def __call__(self, arg: _Markable) -> _Markable:
...
@overload # noqa: F811
def __call__( # noqa: F811
@overload
def __call__(
self,
condition: Union[str, bool] = ...,
*conditions: Union[str, bool],
reason: str = ...,
run: bool = ...,
raises: Union[
"Type[BaseException]", Tuple["Type[BaseException]", ...]
] = ...,
strict: bool = ...
raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,
strict: bool = ...,
) -> MarkDecorator:
...
@@ -437,7 +432,7 @@ if TYPE_CHECKING:
Callable[[Any], Optional[object]],
]
] = ...,
scope: Optional[_Scope] = ...
scope: Optional[_Scope] = ...,
) -> MarkDecorator:
...
@@ -470,18 +465,17 @@ class MarkGenerator:
applies a 'slowtest' :class:`Mark` on ``test_function``.
"""
_config = None # type: Optional[Config]
_markers = set() # type: Set[str]
_config: Optional[Config] = None
_markers: Set[str] = set()
# See TYPE_CHECKING above.
if TYPE_CHECKING:
# TODO(py36): Change to builtin annotation syntax.
skip = _SkipMarkDecorator(Mark("skip", (), {}))
skipif = _SkipifMarkDecorator(Mark("skipif", (), {}))
xfail = _XfailMarkDecorator(Mark("xfail", (), {}))
parametrize = _ParametrizeMarkDecorator(Mark("parametrize", (), {}))
usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures", (), {}))
filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings", (), {}))
skip: _SkipMarkDecorator
skipif: _SkipifMarkDecorator
xfail: _XfailMarkDecorator
parametrize: _ParametrizeMarkDecorator
usefixtures: _UsefixturesMarkDecorator
filterwarnings: _FilterwarningsMarkDecorator
def __getattr__(self, name: str) -> MarkDecorator:
if name[0] == "_":
@@ -502,16 +496,16 @@ class MarkGenerator:
# If the name is not in the set of known marks after updating,
# then it really is time to issue a warning or an error.
if name not in self._markers:
if self._config.option.strict_markers:
if self._config.option.strict_markers or self._config.option.strict:
fail(
"{!r} not found in `markers` configuration option".format(name),
f"{name!r} not found in `markers` configuration option",
pytrace=False,
)
# Raise a specific error for common misspellings of "parametrize".
if name in ["parameterize", "parametrise", "parameterise"]:
__tracebackhide__ = True
fail("Unknown '{}' mark, did you mean 'parametrize'?".format(name))
fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
warnings.warn(
"Unknown pytest.mark.%s - is this a typo? You can register "
@@ -527,9 +521,8 @@ class MarkGenerator:
MARK_GEN = MarkGenerator()
# TODO(py36): inherit from typing.MutableMapping[str, Any].
@final
class NodeKeywords(collections.abc.MutableMapping): # type: ignore[type-arg]
class NodeKeywords(MutableMapping[str, Any]):
def __init__(self, node: "Node") -> None:
self.node = node
self.parent = node.parent
@@ -563,4 +556,4 @@ class NodeKeywords(collections.abc.MutableMapping): # type: ignore[type-arg]
return len(self._seen())
def __repr__(self) -> str:
return "<NodeKeywords for node {}>".format(self.node)
return f"<NodeKeywords for node {self.node}>"

View File

@@ -4,20 +4,20 @@ import re
import sys
import warnings
from contextlib import contextmanager
from pathlib import Path
from typing import Any
from typing import Generator
from typing import List
from typing import MutableMapping
from typing import Optional
from typing import overload
from typing import Tuple
from typing import TypeVar
from typing import Union
import pytest
from _pytest.compat import final
from _pytest.compat import overload
from _pytest.fixtures import fixture
from _pytest.pathlib import Path
from _pytest.warning_types import PytestWarning
RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
@@ -74,7 +74,7 @@ def resolve(name: str) -> object:
if expected == used:
raise
else:
raise ImportError("import error in {}: {}".format(used, ex)) from ex
raise ImportError(f"import error in {used}: {ex}") from ex
found = annotated_getattr(found, part, used)
return found
@@ -93,9 +93,7 @@ def annotated_getattr(obj: object, name: str, ann: str) -> object:
def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]:
if not isinstance(import_path, str) or "." not in import_path: # type: ignore[unreachable]
raise TypeError(
"must be absolute import path string, not {!r}".format(import_path)
)
raise TypeError(f"must be absolute import path string, not {import_path!r}")
module, attr = import_path.rsplit(".", 1)
target = resolve(module)
if raising:
@@ -113,19 +111,27 @@ notset = Notset()
@final
class MonkeyPatch:
"""Object returned by the ``monkeypatch`` fixture keeping a record of
setattr/item/env/syspath changes."""
"""Helper to conveniently monkeypatch attributes/items/environment
variables/syspath.
Returned by the :fixture:`monkeypatch` fixture.
:versionchanged:: 6.2
Can now also be used directly as `pytest.MonkeyPatch()`, for when
the fixture is not available. In this case, use
:meth:`with MonkeyPatch.context() as mp: <context>` or remember to call
:meth:`undo` explicitly.
"""
def __init__(self) -> None:
self._setattr = [] # type: List[Tuple[object, str, object]]
self._setitem = (
[]
) # type: List[Tuple[MutableMapping[Any, Any], object, object]]
self._cwd = None # type: Optional[str]
self._savesyspath = None # type: Optional[List[str]]
self._setattr: List[Tuple[object, str, object]] = []
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = ([])
self._cwd: Optional[str] = None
self._savesyspath: Optional[List[str]] = None
@classmethod
@contextmanager
def context(self) -> Generator["MonkeyPatch", None, None]:
def context(cls) -> Generator["MonkeyPatch", None, None]:
"""Context manager that returns a new :class:`MonkeyPatch` object
which undoes any patching done inside the ``with`` block upon exit.
@@ -144,7 +150,7 @@ class MonkeyPatch:
such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
of this see `#3290 <https://github.com/pytest-dev/pytest/issues/3290>`_.
"""
m = MonkeyPatch()
m = cls()
try:
yield m
finally:
@@ -156,13 +162,13 @@ class MonkeyPatch:
) -> None:
...
@overload # noqa: F811
def setattr( # noqa: F811
@overload
def setattr(
self, target: object, name: str, value: object, raising: bool = ...,
) -> None:
...
def setattr( # noqa: F811
def setattr(
self,
target: Union[str, object],
name: Union[object, str],
@@ -202,7 +208,7 @@ class MonkeyPatch:
oldval = getattr(target, name, notset)
if raising and oldval is notset:
raise AttributeError("{!r} has no attribute {!r}".format(target, name))
raise AttributeError(f"{target!r} has no attribute {name!r}")
# avoid class descriptors like staticmethod/classmethod
if inspect.isclass(target):
@@ -275,7 +281,7 @@ class MonkeyPatch:
"""
if not isinstance(value, str):
warnings.warn( # type: ignore[unreachable]
pytest.PytestWarning(
PytestWarning(
"Value of environment variable {name} type should be str, but got "
"{value!r} (type: {type}); converted to str implicitly".format(
name=name, value=value, type=type(value).__name__
@@ -294,7 +300,7 @@ class MonkeyPatch:
Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
False.
"""
environ = os.environ # type: MutableMapping[str, str]
environ: MutableMapping[str, str] = os.environ
self.delitem(environ, name, raising=raising)
def syspath_prepend(self, path) -> None:

View File

@@ -1,15 +1,16 @@
import os
import warnings
from functools import lru_cache
from typing import Any
from pathlib import Path
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import overload
from typing import Set
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
@@ -20,27 +21,19 @@ from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr
from _pytest.compat import cached_property
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import FixtureLookupError
from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords
from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath
from _pytest.pathlib import Path
from _pytest.store import Store
if TYPE_CHECKING:
from typing import Type
# Imported here due to circular import.
from _pytest.main import Session
from _pytest.warning_types import PytestWarning
from _pytest._code.code import _TracebackStyle
@@ -49,46 +42,39 @@ SEP = "/"
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
@lru_cache(maxsize=None)
def _splitnode(nodeid: str) -> Tuple[str, ...]:
"""Split a nodeid into constituent 'parts'.
def iterparentnodeids(nodeid: str) -> Iterator[str]:
"""Return the parent node IDs of a given node ID, inclusive.
Node IDs are strings, and can be things like:
''
'testing/code'
'testing/code/test_excinfo.py'
'testing/code/test_excinfo.py::TestFormattedExcinfo'
For the node ID
Return values are lists e.g.
[]
['testing', 'code']
['testing', 'code', 'test_excinfo.py']
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo']
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
the result would be
""
"testing"
"testing/code"
"testing/code/test_excinfo.py"
"testing/code/test_excinfo.py::TestFormattedExcinfo"
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
Note that :: parts are only considered at the last / component.
"""
if nodeid == "":
# If there is no root node at all, return an empty list so the caller's
# logic can remain sane.
return ()
parts = nodeid.split(SEP)
# Replace single last element 'test_foo.py::Bar' with multiple elements
# 'test_foo.py', 'Bar'.
parts[-1:] = parts[-1].split("::")
# Convert parts into a tuple to avoid possible errors with caching of a
# mutable type.
return tuple(parts)
def ischildnode(baseid: str, nodeid: str) -> bool:
"""Return True if the nodeid is a child node of the baseid.
E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz',
but not of 'foo/blorp'.
"""
base_parts = _splitnode(baseid)
node_parts = _splitnode(nodeid)
if len(node_parts) < len(base_parts):
return False
return node_parts[: len(base_parts)] == base_parts
pos = 0
sep = SEP
yield ""
while True:
at = nodeid.find(sep, pos)
if at == -1 and sep == SEP:
sep = "::"
elif at == -1:
if nodeid:
yield nodeid
break
else:
if at:
yield nodeid[:at]
pos = at + len(sep)
_NodeType = TypeVar("_NodeType", bound="Node")
@@ -145,7 +131,7 @@ class Node(metaclass=NodeMeta):
#: The pytest config object.
if config:
self.config = config # type: Config
self.config: Config = config
else:
if not parent:
raise TypeError("config or parent must be provided")
@@ -166,13 +152,10 @@ class Node(metaclass=NodeMeta):
self.keywords = NodeKeywords(self)
#: The marker objects belonging to this node.
self.own_markers = [] # type: List[Mark]
self.own_markers: List[Mark] = []
#: Allow adding of extra keywords to use for matching.
self.extra_keyword_matches = set() # type: Set[str]
# Used for storing artificial fixturedefs for direct parametrization.
self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef[Any]]
self.extra_keyword_matches: Set[str] = set()
if nodeid is not None:
assert "::()" not in nodeid
@@ -214,27 +197,31 @@ class Node(metaclass=NodeMeta):
def __repr__(self) -> str:
return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
def warn(self, warning: "PytestWarning") -> None:
def warn(self, warning: Warning) -> None:
"""Issue a warning for this Node.
Warnings will be displayed after the test session, unless explicitly suppressed.
:param Warning warning:
The warning instance to issue. Must be a subclass of PytestWarning.
The warning instance to issue.
:raises ValueError: If ``warning`` instance is not a subclass of PytestWarning.
:raises ValueError: If ``warning`` instance is not a subclass of Warning.
Example usage:
.. code-block:: python
node.warn(PytestWarning("some message"))
"""
from _pytest.warning_types import PytestWarning
node.warn(UserWarning("some message"))
if not isinstance(warning, PytestWarning):
.. versionchanged:: 6.2
Any subclass of :class:`Warning` is now accepted, rather than only
:class:`PytestWarning <pytest.PytestWarning>` subclasses.
"""
# enforce type checks here to avoid getting a generic type error later otherwise.
if not isinstance(warning, Warning):
raise ValueError(
"warning must be an instance of PytestWarning or subclass, got {!r}".format(
"warning must be an instance of Warning or subclass, got {!r}".format(
warning
)
)
@@ -264,7 +251,7 @@ class Node(metaclass=NodeMeta):
"""Return list of all parent collectors up to self, starting from
the root of collection tree."""
chain = []
item = self # type: Optional[Node]
item: Optional[Node] = self
while item is not None:
chain.append(item)
item = item.parent
@@ -317,11 +304,11 @@ class Node(metaclass=NodeMeta):
def get_closest_marker(self, name: str) -> Optional[Mark]:
...
@overload # noqa: F811
def get_closest_marker(self, name: str, default: Mark) -> Mark: # noqa: F811
@overload
def get_closest_marker(self, name: str, default: Mark) -> Mark:
...
def get_closest_marker( # noqa: F811
def get_closest_marker(
self, name: str, default: Optional[Mark] = None
) -> Optional[Mark]:
"""Return the first marker matching the name, from closest (for
@@ -334,7 +321,7 @@ class Node(metaclass=NodeMeta):
def listextrakeywords(self) -> Set[str]:
"""Return a set of all extra keywords in self and any parents."""
extra_keywords = set() # type: Set[str]
extra_keywords: Set[str] = set()
for item in self.listchain():
extra_keywords.update(item.extra_keyword_matches)
return extra_keywords
@@ -350,10 +337,10 @@ class Node(metaclass=NodeMeta):
"""
self.session._setupstate.addfinalizer(fin, self)
def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]:
def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]:
"""Get the next parent node (including self) which is an instance of
the given class."""
current = self # type: Optional[Node]
current: Optional[Node] = self
while current and not isinstance(current, cls):
current = current.parent
assert current is None or isinstance(current, cls)
@@ -367,6 +354,8 @@ class Node(metaclass=NodeMeta):
excinfo: ExceptionInfo[BaseException],
style: "Optional[_TracebackStyle]" = None,
) -> TerminalRepr:
from _pytest.fixtures import FixtureLookupError
if isinstance(excinfo.value, ConftestImportFailure):
excinfo = ExceptionInfo(excinfo.value.excinfo)
if isinstance(excinfo.value, fail.Exception):
@@ -439,9 +428,7 @@ def get_fslocation_from_item(
:rtype: A tuple of (str|py.path.local, int) with filename and line number.
"""
# See Item.location.
location = getattr(
node, "location", None
) # type: Optional[Tuple[str, Optional[int], str]]
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
if location is not None:
return location[:2]
obj = getattr(node, "obj", None)
@@ -566,11 +553,11 @@ class Item(Node):
nodeid: Optional[str] = None,
) -> None:
super().__init__(name, parent, config, session, nodeid=nodeid)
self._report_sections = [] # type: List[Tuple[str, str, str]]
self._report_sections: List[Tuple[str, str, str]] = []
#: A list of tuples (name, value) that holds user defined properties
#: for this test.
self.user_properties = [] # type: List[Tuple[str, object]]
self.user_properties: List[Tuple[str, object]] = []
def runtest(self) -> None:
raise NotImplementedError("runtest must be implemented by Item subclass")

View File

@@ -5,13 +5,13 @@ from typing import Any
from typing import Callable
from typing import cast
from typing import Optional
from typing import Type
from typing import TypeVar
TYPE_CHECKING = False # Avoid circular import through compat.
if TYPE_CHECKING:
from typing import NoReturn
from typing import Type # noqa: F401 (used in type string)
from typing_extensions import Protocol
else:
# typing.Protocol is only available starting from Python 3.8. It is also
@@ -38,9 +38,9 @@ class OutcomeException(BaseException):
self.pytrace = pytrace
def __repr__(self) -> str:
if self.msg:
if self.msg is not None:
return self.msg
return "<{} instance>".format(self.__class__.__name__)
return f"<{self.__class__.__name__} instance>"
__str__ = __repr__
@@ -84,12 +84,12 @@ class Exit(Exception):
# Ideally would just be `exit.Exception = Exit` etc.
_F = TypeVar("_F", bound=Callable[..., object])
_ET = TypeVar("_ET", bound="Type[BaseException]")
_ET = TypeVar("_ET", bound=Type[BaseException])
class _WithException(Protocol[_F, _ET]):
Exception = None # type: _ET
__call__ = None # type: _F
Exception: _ET
__call__: _F
def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]:
@@ -208,7 +208,7 @@ def importorskip(
__import__(modname)
except ImportError as exc:
if reason is None:
reason = "could not import {!r}: {}".format(modname, exc)
reason = f"could not import {modname!r}: {exc}"
raise Skipped(reason, allow_module_level=True) from None
mod = sys.modules[modname]
if minversion is None:

View File

@@ -79,9 +79,9 @@ def create_new_paste(contents: Union[str, bytes]) -> str:
params = {"code": contents, "lexer": "text", "expiry": "1week"}
url = "https://bpaste.net"
try:
response = (
response: str = (
urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
) # type: str
)
except OSError as exc_info: # urllib errors
return "bad response: %s" % exc_info
m = re.search(r'href="/raw/(\w+)"', response)
@@ -107,4 +107,4 @@ def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
s = file.getvalue()
assert len(s)
pastebinurl = create_new_paste(s)
terminalreporter.write_line("{} --> {}".format(msg, pastebinurl))
terminalreporter.write_line(f"{msg} --> {pastebinurl}")

View File

@@ -9,11 +9,17 @@ import sys
import uuid
import warnings
from enum import Enum
from errno import EBADF
from errno import ELOOP
from errno import ENOENT
from errno import ENOTDIR
from functools import partial
from os.path import expanduser
from os.path import expandvars
from os.path import isabs
from os.path import sep
from pathlib import Path
from pathlib import PurePath
from posixpath import sep as posix_sep
from types import ModuleType
from typing import Callable
@@ -30,19 +36,29 @@ from _pytest.compat import assert_never
from _pytest.outcomes import skip
from _pytest.warning_types import PytestWarning
if sys.version_info[:2] >= (3, 6):
from pathlib import Path, PurePath
else:
from pathlib2 import Path, PurePath
__all__ = ["Path", "PurePath"]
LOCK_TIMEOUT = 60 * 60 * 3
LOCK_TIMEOUT = 60 * 60 * 24 * 3
_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
# The following function, variables and comments were
# copied from cpython 3.9 Lib/pathlib.py file.
# EBADF - guard against macOS `stat` throwing EBADF
_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP)
_IGNORED_WINERRORS = (
21, # ERROR_NOT_READY - drive exists but is not accessible
1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself
)
def _ignore_error(exception):
return (
getattr(exception, "errno", None) in _IGNORED_ERRORS
or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
)
def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
return path.joinpath(".lock")
@@ -69,9 +85,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
if not isinstance(excvalue, PermissionError):
warnings.warn(
PytestWarning(
"(rm_rf) error removing {}\n{}: {}".format(path, exctype, excvalue)
)
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
)
return False
@@ -206,7 +220,7 @@ def make_numbered_dir(root: Path, prefix: str) -> Path:
# try up to 10 times to create the folder
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
new_number = max_existing + 1
new_path = root.joinpath("{}{}".format(prefix, new_number))
new_path = root.joinpath(f"{prefix}{new_number}")
try:
new_path.mkdir()
except Exception:
@@ -227,7 +241,7 @@ def create_cleanup_lock(p: Path) -> Path:
try:
fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
except FileExistsError as e:
raise OSError("cannot create lockfile in {path}".format(path=p)) from e
raise OSError(f"cannot create lockfile in {p}") from e
else:
pid = os.getpid()
spid = str(pid).encode()
@@ -264,7 +278,7 @@ def maybe_delete_a_numbered_dir(path: Path) -> None:
lock_path = create_cleanup_lock(path)
parent = path.parent
garbage = parent.joinpath("garbage-{}".format(uuid.uuid4()))
garbage = parent.joinpath(f"garbage-{uuid.uuid4()}")
path.rename(garbage)
rm_rf(garbage)
except OSError:
@@ -407,7 +421,7 @@ def fnmatch_ex(pattern: str, path) -> bool:
else:
name = str(path)
if path.is_absolute() and not os.path.isabs(pattern):
pattern = "*{}{}".format(os.sep, pattern)
pattern = f"*{os.sep}{pattern}"
return fnmatch.fnmatch(name, pattern)
@@ -421,7 +435,7 @@ def symlink_or_skip(src, dst, **kwargs):
try:
os.symlink(str(src), str(dst), **kwargs)
except OSError as e:
skip("symlinks not supported: {}".format(e))
skip(f"symlinks not supported: {e}")
class ImportMode(Enum):
@@ -444,7 +458,7 @@ class ImportPathMismatchError(ImportError):
def import_path(
p: Union[str, py.path.local, Path],
*,
mode: Union[str, ImportMode] = ImportMode.prepend
mode: Union[str, ImportMode] = ImportMode.prepend,
) -> ModuleType:
"""Import and return a module from the given path, which can be a file (a module) or
a directory (a package).
@@ -529,7 +543,7 @@ def import_path(
module_file = module_file[: -(len(os.path.sep + "__init__.py"))]
try:
is_same = os.path.samefile(str(path), module_file)
is_same = _is_same(str(path), module_file)
except FileNotFoundError:
is_same = False
@@ -539,6 +553,20 @@ def import_path(
return mod
# Implement a special _is_same function on Windows which returns True if the two filenames
# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
if sys.platform.startswith("win"):
def _is_same(f1: str, f2: str) -> bool:
return Path(f1) == Path(f2) or os.path.samefile(f1, f2)
else:
def _is_same(f1: str, f2: str) -> bool:
return os.path.samefile(f1, f2)
def resolve_package_path(path: Path) -> Optional[Path]:
"""Return the Python package path by looking for the last
directory upwards which still contains an __init__.py.
@@ -563,10 +591,25 @@ def visit(
Entries at each directory level are sorted.
"""
entries = sorted(os.scandir(path), key=lambda entry: entry.name)
# Skip entries with symlink loops and other brokenness, so the caller doesn't
# have to deal with it.
entries = []
for entry in os.scandir(path):
try:
entry.is_file()
except OSError as err:
if _ignore_error(err):
continue
raise
entries.append(entry)
entries.sort(key=lambda entry: entry.name)
yield from entries
for entry in entries:
if entry.is_dir(follow_symlinks=False) and recurse(entry):
if entry.is_dir() and recurse(entry):
yield from visit(entry.path, recurse)
@@ -581,7 +624,10 @@ def absolutepath(path: Union[Path, str]) -> Path:
def commonpath(path1: Path, path2: Path) -> Optional[Path]:
"""Return the common part shared with the other path, or None if there is
no common part."""
no common part.
If one path is relative and one is absolute, returns None.
"""
try:
return Path(os.path.commonpath((str(path1), str(path2))))
except ValueError:
@@ -592,13 +638,17 @@ def bestrelpath(directory: Path, dest: Path) -> str:
"""Return a string which is a relative path from directory to dest such
that directory/bestrelpath == dest.
The paths must be either both absolute or both relative.
If no such path can be determined, returns dest.
"""
if dest == directory:
return os.curdir
# Find the longest common directory.
base = commonpath(directory, dest)
# Can be the case on Windows.
# Can be the case on Windows for two absolute paths on different drives.
# Can be the case for two relative paths without common prefix.
# Can be the case for a relative path and an absolute path.
if not base:
return str(dest)
reldirectory = directory.relative_to(base)

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More