Compare commits
334 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0b088b52e | ||
|
|
e5a3c870b4 | ||
|
|
7d4c4c66d4 | ||
|
|
939a792c41 | ||
|
|
17644ff285 | ||
|
|
64faa41d06 | ||
|
|
ca1bb9a3a1 | ||
|
|
78ef531420 | ||
|
|
212ee450b7 | ||
|
|
c1c08852f9 | ||
|
|
e06a077ac2 | ||
|
|
cb77e65c97 | ||
|
|
6367f0f5f1 | ||
|
|
b88e09a697 | ||
|
|
68bbd42213 | ||
|
|
22ee2093b8 | ||
|
|
87a99275fb | ||
|
|
abbd7c30a4 | ||
|
|
abae60c8d0 | ||
|
|
e92893ed24 | ||
|
|
27b5435a40 | ||
|
|
50db718a6a | ||
|
|
bfd0addaeb | ||
|
|
be11d3e195 | ||
|
|
266f05c4c4 | ||
|
|
220288ac77 | ||
|
|
4d8903fd0b | ||
|
|
67106f056b | ||
|
|
5d3c5123f8 | ||
|
|
74d9f56d0f | ||
|
|
051db6a33d | ||
|
|
aa358433b0 | ||
|
|
e723069165 | ||
|
|
855fd17014 | ||
|
|
d11781920b | ||
|
|
2c0d2eef40 | ||
|
|
ef8ec01e39 | ||
|
|
0a1c2a7ca1 | ||
|
|
fe0a76e1a6 | ||
|
|
dcafb8c48c | ||
|
|
a76cc8f8c4 | ||
|
|
4d2fa581e1 | ||
|
|
f7a3f45a18 | ||
|
|
dff7b203f7 | ||
|
|
4705fd2bbe | ||
|
|
ca0476953e | ||
|
|
7e92930fa9 | ||
|
|
33769d0328 | ||
|
|
5db2e6c7a1 | ||
|
|
804fc4063a | ||
|
|
82a2174867 | ||
|
|
a80e031c62 | ||
|
|
452e5c1cf0 | ||
|
|
c6b11b9f62 | ||
|
|
b8255308d6 | ||
|
|
a5c0fb7f6b | ||
|
|
f25683354e | ||
|
|
7d13599ba1 | ||
|
|
43664d7841 | ||
|
|
2a2f888909 | ||
|
|
ad5ddaf55a | ||
|
|
4588130c1e | ||
|
|
5003bae0de | ||
|
|
6e32a1f73d | ||
|
|
611d254ed5 | ||
|
|
57a8f208bc | ||
|
|
fcdc1d867e | ||
|
|
098dca3a9f | ||
|
|
8e2ed76227 | ||
|
|
bf7c188cc0 | ||
|
|
8c9efd8608 | ||
|
|
e1ad1a14af | ||
|
|
327fe4cfcc | ||
|
|
d02491931a | ||
|
|
032db159c9 | ||
|
|
cd2085ee71 | ||
|
|
ad305e71d7 | ||
|
|
7d8688d54b | ||
|
|
253419316c | ||
|
|
997ef59306 | ||
|
|
60b1913ba2 | ||
|
|
2c09930b6d | ||
|
|
d461e931dd | ||
|
|
eada0b1fd7 | ||
|
|
150535b6c1 | ||
|
|
f1ec02cdcd | ||
|
|
9f5d73d44a | ||
|
|
8609f8d25a | ||
|
|
953a618102 | ||
|
|
cf6d8e7e53 | ||
|
|
8af78f417f | ||
|
|
535fd1f311 | ||
|
|
762eaf443a | ||
|
|
330640eb96 | ||
|
|
e3d412d1f4 | ||
|
|
6f9a12a8a3 | ||
|
|
0e47599572 | ||
|
|
c480223e88 | ||
|
|
0b522d40a7 | ||
|
|
d900a6c8bd | ||
|
|
fe46fbb719 | ||
|
|
8d401cdb9d | ||
|
|
3f3f6f1be4 | ||
|
|
b6da5cc54c | ||
|
|
eaeeedc9c3 | ||
|
|
317cd41215 | ||
|
|
7f27512a48 | ||
|
|
bf127a63b2 | ||
|
|
fe16f81da1 | ||
|
|
d0ba242c46 | ||
|
|
fe06be8590 | ||
|
|
79b4ca92d8 | ||
|
|
57b0c60cb4 | ||
|
|
6e57d123bb | ||
|
|
011f88f7e7 | ||
|
|
2eb9301ad5 | ||
|
|
f0db64ac2e | ||
|
|
514ca6f4ad | ||
|
|
b7419bd9bb | ||
|
|
2e344d4d63 | ||
|
|
f8749eeb5c | ||
|
|
f76142508f | ||
|
|
be2afb950a | ||
|
|
19de1b7f29 | ||
|
|
f5165064ee | ||
|
|
c9a0881309 | ||
|
|
5167933395 | ||
|
|
75db608479 | ||
|
|
7bff5866b1 | ||
|
|
9720c3301a | ||
|
|
254689ff83 | ||
|
|
21f5222784 | ||
|
|
0bb29d5649 | ||
|
|
db33f03c15 | ||
|
|
ac9ceaacd8 | ||
|
|
771d3e8f4f | ||
|
|
82a11e6207 | ||
|
|
a1c3df1889 | ||
|
|
a821af6b1c | ||
|
|
d2fe619120 | ||
|
|
58e77f58bd | ||
|
|
6a4fa4f485 | ||
|
|
5be03bff61 | ||
|
|
e9fd038aae | ||
|
|
a8464a95ce | ||
|
|
a0b0c37feb | ||
|
|
35ffd29404 | ||
|
|
0565a7a4e1 | ||
|
|
6c3713226c | ||
|
|
f6ceedd15b | ||
|
|
3e599dc149 | ||
|
|
54fbc6f6e1 | ||
|
|
aa47b64e2a | ||
|
|
251adbf644 | ||
|
|
2c4759ce57 | ||
|
|
4dfe2eee94 | ||
|
|
5226c7fac3 | ||
|
|
593b451373 | ||
|
|
898544e147 | ||
|
|
61301d934e | ||
|
|
af0059079c | ||
|
|
9ef7878cbc | ||
|
|
4ae93a7a07 | ||
|
|
d4faa4056b | ||
|
|
42bbb4fa8a | ||
|
|
12c5b6104c | ||
|
|
aa9d1ad2eb | ||
|
|
49e82a4be8 | ||
|
|
803302e70c | ||
|
|
05f1d0d3ef | ||
|
|
1cd62f8c38 | ||
|
|
a522fc745a | ||
|
|
303133f013 | ||
|
|
d26a596072 | ||
|
|
f359b50fe5 | ||
|
|
18b2fc11ad | ||
|
|
d7b722e2ae | ||
|
|
1e94ac784f | ||
|
|
027d2336b8 | ||
|
|
3c19370cec | ||
|
|
7e8f7bfa16 | ||
|
|
3f5e06ecc4 | ||
|
|
50f030d233 | ||
|
|
7696d5371a | ||
|
|
b64c26674d | ||
|
|
63b25304c3 | ||
|
|
73d787df3a | ||
|
|
fa3161011a | ||
|
|
2921ca6e64 | ||
|
|
43c0346d68 | ||
|
|
3ed8e28ef3 | ||
|
|
b84a646389 | ||
|
|
8232fd1a2d | ||
|
|
f52a5d3be2 | ||
|
|
b815f67e65 | ||
|
|
55ebf261ce | ||
|
|
067de257e1 | ||
|
|
4a925ef5e9 | ||
|
|
4afb8c428b | ||
|
|
2f1a2cf07f | ||
|
|
6cc4fe2412 | ||
|
|
10a8691eca | ||
|
|
b75320ba95 | ||
|
|
b50911285a | ||
|
|
a43205b4bc | ||
|
|
bc268a58d1 | ||
|
|
0b70477930 | ||
|
|
8801162275 | ||
|
|
a604a71185 | ||
|
|
66fa6bb42e | ||
|
|
713d32c4da | ||
|
|
57198d477b | ||
|
|
533f4cc10c | ||
|
|
a46b94950c | ||
|
|
952bbefaac | ||
|
|
54d3cd587d | ||
|
|
2b75a311a7 | ||
|
|
26e1784e52 | ||
|
|
dad3e77319 | ||
|
|
3a1c15316b | ||
|
|
f7c929c932 | ||
|
|
b48f1d378b | ||
|
|
a6636fddcd | ||
|
|
738933938a | ||
|
|
342f2cdc17 | ||
|
|
5bd5b8c68a | ||
|
|
0fd86ec8a8 | ||
|
|
4ae7e9788c | ||
|
|
5582ad0445 | ||
|
|
982b614010 | ||
|
|
7845ab4bc3 | ||
|
|
8680dfc939 | ||
|
|
76ac670f7d | ||
|
|
7b47dfb744 | ||
|
|
3c73d6298a | ||
|
|
c220fb235a | ||
|
|
1dc5e97ac2 | ||
|
|
e9371a58a0 | ||
|
|
a48c47b53b | ||
|
|
ea379ba10f | ||
|
|
9a56bb05be | ||
|
|
17e01993d9 | ||
|
|
8a6345515b | ||
|
|
581d49635e | ||
|
|
ef1b91ba87 | ||
|
|
e860ff7299 | ||
|
|
0672bc633f | ||
|
|
2dfb52f7e0 | ||
|
|
cc6eb9f83c | ||
|
|
643e5a9c44 | ||
|
|
b8486037d3 | ||
|
|
78a82c05ef | ||
|
|
853975d93b | ||
|
|
6b239263da | ||
|
|
89e0a3ec27 | ||
|
|
f93995e15c | ||
|
|
f940967e23 | ||
|
|
cbaa7dd56a | ||
|
|
8133d1955e | ||
|
|
738b715655 | ||
|
|
1d5316a28b | ||
|
|
0030ceb11c | ||
|
|
5b186cd609 | ||
|
|
5a156b3645 | ||
|
|
42b3125783 | ||
|
|
50a0d4fa95 | ||
|
|
deff54aae0 | ||
|
|
22a2734d9a | ||
|
|
17985b893d | ||
|
|
1d55c49a9a | ||
|
|
95f00de0df | ||
|
|
c4c666cbc4 | ||
|
|
ee30bf45c9 | ||
|
|
603df1ea1c | ||
|
|
abbf73ad1a | ||
|
|
1226cdab47 | ||
|
|
ab80e0fba0 | ||
|
|
fb992a0c81 | ||
|
|
23581d44bd | ||
|
|
c7eb53317b | ||
|
|
8cc210708c | ||
|
|
4970f6d5c2 | ||
|
|
f883628939 | ||
|
|
40839b5a3a | ||
|
|
5e56d77542 | ||
|
|
de98939ebf | ||
|
|
0d3914b626 | ||
|
|
eb94bce3e2 | ||
|
|
b897008887 | ||
|
|
630428c611 | ||
|
|
8b4c59e606 | ||
|
|
998d540b73 | ||
|
|
c672bfa32e | ||
|
|
8f1d8ac970 | ||
|
|
1ac1ee6fcb | ||
|
|
53d4710c62 | ||
|
|
31f089db6a | ||
|
|
b7b9c54d27 | ||
|
|
e86fe38144 | ||
|
|
5b26178397 | ||
|
|
130100bba1 | ||
|
|
93e55ad2fa | ||
|
|
9d60cf25c0 | ||
|
|
9e32b6ae48 | ||
|
|
99402cf1c0 | ||
|
|
3ac2ae3c8c | ||
|
|
ea906056fa | ||
|
|
c081c5ee23 | ||
|
|
3dcdaab103 | ||
|
|
3615977608 | ||
|
|
94c41bec64 | ||
|
|
8d072205e9 | ||
|
|
b5102d03a6 | ||
|
|
791bb3502c | ||
|
|
eb0c6a8287 | ||
|
|
9ddd573774 | ||
|
|
f99da9058f | ||
|
|
7d0dba18de | ||
|
|
6fc7f07a80 | ||
|
|
05b5b64379 | ||
|
|
229c8e551d | ||
|
|
d483b401ee | ||
|
|
acacf75f49 | ||
|
|
f8350c6304 | ||
|
|
fedc78522b | ||
|
|
b0474398ec | ||
|
|
dc90c9108f | ||
|
|
69031d0033 | ||
|
|
e44d4e6508 | ||
|
|
c416b1d935 | ||
|
|
7d923c389e | ||
|
|
c02e8d8b0d | ||
|
|
35df2cdbee | ||
|
|
2b1410895e |
149
.github/labels.toml
vendored
Normal file
149
.github/labels.toml
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
["os: cygwin"]
|
||||
color = "006b75"
|
||||
description = "cygwin platform-specific problem"
|
||||
name = "os: cygwin"
|
||||
|
||||
["os: linux"]
|
||||
color = "1d76db"
|
||||
description = "linux platform-specific problem"
|
||||
name = "os: linux"
|
||||
|
||||
["os: mac"]
|
||||
color = "bfdadc"
|
||||
description = "mac platform-specific problem"
|
||||
name = "os: mac"
|
||||
|
||||
["os: windows"]
|
||||
color = "fbca04"
|
||||
description = "windows platform-specific problem"
|
||||
name = "os: windows"
|
||||
|
||||
["plugin: argcomplete"]
|
||||
color = "d4c5f9"
|
||||
description = "related to the argcomplete builtin plugin"
|
||||
name = "plugin: argcomplete"
|
||||
|
||||
["plugin: cache"]
|
||||
color = "c7def8"
|
||||
description = "related to the cache builtin plugin"
|
||||
name = "plugin: cache"
|
||||
|
||||
["plugin: capture"]
|
||||
color = "1d76db"
|
||||
description = "related to the capture builtin plugin"
|
||||
name = "plugin: capture"
|
||||
|
||||
["plugin: debugging"]
|
||||
color = "dd52a8"
|
||||
description = "related to the debugging builtin plugin"
|
||||
name = "plugin: debugging"
|
||||
|
||||
["plugin: doctests"]
|
||||
color = "fad8c7"
|
||||
description = "related to the doctests builtin plugin"
|
||||
name = "plugin: doctests"
|
||||
|
||||
["plugin: junitxml"]
|
||||
color = "c5def5"
|
||||
description = "related to the junitxml builtin plugin"
|
||||
name = "plugin: junitxml"
|
||||
|
||||
["plugin: logging"]
|
||||
color = "ff5432"
|
||||
description = "related to the logging builtin plugin"
|
||||
name = "plugin: logging"
|
||||
|
||||
["plugin: monkeypatch"]
|
||||
color = "0e8a16"
|
||||
description = "related to the monkeypatch builtin plugin"
|
||||
name = "plugin: monkeypatch"
|
||||
|
||||
["plugin: nose"]
|
||||
color = "bfdadc"
|
||||
description = "related to the nose integration builtin plugin"
|
||||
name = "plugin: nose"
|
||||
|
||||
["plugin: pastebin"]
|
||||
color = "bfd4f2"
|
||||
description = "related to the pastebin builtin plugin"
|
||||
name = "plugin: pastebin"
|
||||
|
||||
["plugin: pytester"]
|
||||
color = "c5def5"
|
||||
description = "related to the pytester builtin plugin"
|
||||
name = "plugin: pytester"
|
||||
|
||||
["plugin: tmpdir"]
|
||||
color = "bfd4f2"
|
||||
description = "related to the tmpdir builtin plugin"
|
||||
name = "plugin: tmpdir"
|
||||
|
||||
["plugin: unittest"]
|
||||
color = "006b75"
|
||||
description = "related to the unittest integration builtin plugin"
|
||||
name = "plugin: unittest"
|
||||
|
||||
["plugin: warnings"]
|
||||
color = "fef2c0"
|
||||
description = "related to the warnings builtin plugin"
|
||||
name = "plugin: warnings"
|
||||
|
||||
["plugin: xdist"]
|
||||
color = "5319e7"
|
||||
description = "related to the xdist external plugin"
|
||||
name = "plugin: xdist"
|
||||
|
||||
["status: critical"]
|
||||
color = "e11d21"
|
||||
description = "grave problem or usability issue that affects lots of users"
|
||||
name = "status: critical"
|
||||
|
||||
["status: easy"]
|
||||
color = "bfe5bf"
|
||||
description = "easy issue that is friendly to new contributor"
|
||||
name = "status: easy"
|
||||
|
||||
["status: help wanted"]
|
||||
color = "159818"
|
||||
description = "developers would like help from experts on this topic"
|
||||
name = "status: help wanted"
|
||||
|
||||
["status: needs information"]
|
||||
color = "5319e7"
|
||||
description = "reporter needs to provide more information; can be closed after 2 or more weeks of inactivity"
|
||||
name = "status: needs information"
|
||||
|
||||
["topic: collection"]
|
||||
color = "006b75"
|
||||
description = "related to the collection phase"
|
||||
name = "topic: collection"
|
||||
|
||||
["topic: config"]
|
||||
color = "006b75"
|
||||
description = "related to config handling, argument parsing and config file"
|
||||
name = "topic: config"
|
||||
|
||||
["topic: fixtures"]
|
||||
color = "5319e7"
|
||||
description = "anything involving fixtures directly or indirectly"
|
||||
name = "topic: fixtures"
|
||||
|
||||
["topic: marks"]
|
||||
color = "b60205"
|
||||
description = "related to marks, either the general marks or builtin"
|
||||
name = "topic: marks"
|
||||
|
||||
["topic: parametrize"]
|
||||
color = "fbca04"
|
||||
description = "related to @pytest.mark.parametrize"
|
||||
name = "topic: parametrize"
|
||||
|
||||
["topic: reporting"]
|
||||
color = "fef2c0"
|
||||
description = "related to terminal output and user-facing messages and errors"
|
||||
name = "topic: reporting"
|
||||
|
||||
["topic: rewrite"]
|
||||
color = "0e8a16"
|
||||
description = "related to the assertion rewrite mechanism"
|
||||
name = "topic: rewrite"
|
||||
@@ -1,19 +1,19 @@
|
||||
exclude: doc/en/example/py2py3/test_py2.py
|
||||
repos:
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: 18.4a4
|
||||
rev: 18.6b4
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
language_version: python3.6
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: v0.1.1
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==18.5b1]
|
||||
additional_dependencies: [black==18.6b4]
|
||||
language_version: python3.6
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v1.2.3
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -25,6 +25,10 @@ repos:
|
||||
rev: v1.2.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: rst-backticks
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: rst
|
||||
@@ -34,3 +38,8 @@ repos:
|
||||
language: python
|
||||
additional_dependencies: [pygments, restructuredtext_lint]
|
||||
python_version: python3.6
|
||||
- id: changelogs-rst
|
||||
name: changelog files must end in .rst
|
||||
entry: ./scripts/fail
|
||||
language: script
|
||||
files: 'changelog/.*(?<!\.rst)$'
|
||||
|
||||
11
.travis.yml
11
.travis.yml
@@ -3,7 +3,8 @@ language: python
|
||||
stages:
|
||||
- linting
|
||||
- test
|
||||
- deploy
|
||||
- name: deploy
|
||||
if: repo = pytest-dev/pytest AND tag IS present
|
||||
python:
|
||||
- '3.6'
|
||||
install:
|
||||
@@ -37,10 +38,12 @@ jobs:
|
||||
python: 'pypy-5.4'
|
||||
- env: TOXENV=py35
|
||||
python: '3.5'
|
||||
- env: TOXENV=py35-freeze
|
||||
python: '3.5'
|
||||
- env: TOXENV=py36-freeze
|
||||
python: '3.6'
|
||||
- env: TOXENV=py37
|
||||
python: 'nightly'
|
||||
python: '3.7'
|
||||
sudo: required
|
||||
dist: xenial
|
||||
|
||||
- stage: deploy
|
||||
python: '3.6'
|
||||
|
||||
7
AUTHORS
7
AUTHORS
@@ -89,6 +89,7 @@ Hugo van Kemenade
|
||||
Hui Wang (coldnight)
|
||||
Ian Bicking
|
||||
Ian Lesperance
|
||||
Ionuț Turturică
|
||||
Jaap Broekhuizen
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
@@ -126,6 +127,7 @@ Maik Figura
|
||||
Mandeep Bhutani
|
||||
Manuel Krebber
|
||||
Marc Schlaich
|
||||
Marcelo Duarte Trevisani
|
||||
Marcin Bachry
|
||||
Mark Abramowitz
|
||||
Markus Unterwaditzer
|
||||
@@ -157,6 +159,7 @@ Oleg Sushchenko
|
||||
Oliver Bestwalter
|
||||
Omar Kohl
|
||||
Omer Hadari
|
||||
Ondřej Súkup
|
||||
Patrick Hayes
|
||||
Paweł Adamczak
|
||||
Pedro Algarvio
|
||||
@@ -179,7 +182,9 @@ Russel Winder
|
||||
Ryan Wooden
|
||||
Samuel Dion-Girardeau
|
||||
Samuele Pedroni
|
||||
Sankt Petersbug
|
||||
Segev Finer
|
||||
Serhii Mozghovyi
|
||||
Simon Gomizelj
|
||||
Skylar Downes
|
||||
Srinivas Reddy Thatiparthy
|
||||
@@ -188,6 +193,7 @@ Stefan Zimmermann
|
||||
Stefano Taschini
|
||||
Steffen Allner
|
||||
Stephan Obermann
|
||||
Tadek Teleżyński
|
||||
Tarcisio Fischer
|
||||
Tareq Alayan
|
||||
Ted Xiao
|
||||
@@ -204,6 +210,7 @@ Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
Vitaly Lashmanov
|
||||
Vlad Dragos
|
||||
Wil Cooley
|
||||
William Lee
|
||||
Wouter van Ackooy
|
||||
Xuan Luong
|
||||
|
||||
273
CHANGELOG.rst
273
CHANGELOG.rst
@@ -1,3 +1,13 @@
|
||||
=================
|
||||
Changelog history
|
||||
=================
|
||||
|
||||
Versions follow `Semantic Versioning <https://semver.org/>`_ (``<major>.<minor>.<patch>``).
|
||||
|
||||
Backward incompatible (breaking) changes will only be introduced in major versions
|
||||
with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
|
||||
..
|
||||
You should *NOT* be adding new change log entries to this file, this
|
||||
file is managed by towncrier. You *may* edit previous change logs to
|
||||
@@ -8,7 +18,206 @@
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
Pytest 3.6.2 (2018-06-20)
|
||||
pytest 3.7.2 (2018-08-16)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3671 <https://github.com/pytest-dev/pytest/issues/3671>`_: Fix ``filterwarnings`` not being registered as a builtin mark.
|
||||
|
||||
|
||||
- `#3768 <https://github.com/pytest-dev/pytest/issues/3768>`_, `#3789 <https://github.com/pytest-dev/pytest/issues/3789>`_: Fix test collection from packages mixed with normal directories.
|
||||
|
||||
|
||||
- `#3771 <https://github.com/pytest-dev/pytest/issues/3771>`_: Fix infinite recursion during collection if a ``pytest_ignore_collect`` hook returns ``False`` instead of ``None``.
|
||||
|
||||
|
||||
- `#3774 <https://github.com/pytest-dev/pytest/issues/3774>`_: Fix bug where decorated fixtures would lose functionality (for example ``@mock.patch``).
|
||||
|
||||
|
||||
- `#3775 <https://github.com/pytest-dev/pytest/issues/3775>`_: Fix bug where importing modules or other objects with prefix ``pytest_`` prefix would raise a ``PluginValidationError``.
|
||||
|
||||
|
||||
- `#3788 <https://github.com/pytest-dev/pytest/issues/3788>`_: Fix ``AttributeError`` during teardown of ``TestCase`` subclasses which raise an exception during ``__init__``.
|
||||
|
||||
|
||||
- `#3804 <https://github.com/pytest-dev/pytest/issues/3804>`_: Fix traceback reporting for exceptions with ``__cause__`` cycles.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3746 <https://github.com/pytest-dev/pytest/issues/3746>`_: Add documentation for ``metafunc.config`` that had been mistakenly hidden.
|
||||
|
||||
|
||||
pytest 3.7.1 (2018-08-02)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3473 <https://github.com/pytest-dev/pytest/issues/3473>`_: Raise immediately if ``approx()`` is given an expected value of a type it doesn't understand (e.g. strings, nested dicts, etc.).
|
||||
|
||||
|
||||
- `#3712 <https://github.com/pytest-dev/pytest/issues/3712>`_: Correctly represent the dimensions of an numpy array when calling ``repr()`` on ``approx()``.
|
||||
|
||||
- `#3742 <https://github.com/pytest-dev/pytest/issues/3742>`_: Fix incompatibility with third party plugins during collection, which produced the error ``object has no attribute '_collectfile'``.
|
||||
|
||||
- `#3745 <https://github.com/pytest-dev/pytest/issues/3745>`_: Display the absolute path if ``cache_dir`` is not relative to the ``rootdir`` instead of failing.
|
||||
|
||||
|
||||
- `#3747 <https://github.com/pytest-dev/pytest/issues/3747>`_: Fix compatibility problem with plugins and the warning code issued by fixture functions when they are called directly.
|
||||
|
||||
|
||||
- `#3748 <https://github.com/pytest-dev/pytest/issues/3748>`_: Fix infinite recursion in ``pytest.approx`` with arrays in ``numpy<1.13``.
|
||||
|
||||
|
||||
- `#3757 <https://github.com/pytest-dev/pytest/issues/3757>`_: Pin pathlib2 to ``>=2.2.0`` as we require ``__fspath__`` support.
|
||||
|
||||
|
||||
- `#3763 <https://github.com/pytest-dev/pytest/issues/3763>`_: Fix ``TypeError`` when the assertion message is ``bytes`` in python 3.
|
||||
|
||||
|
||||
pytest 3.7.0 (2018-07-30)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- `#2639 <https://github.com/pytest-dev/pytest/issues/2639>`_: ``pytest_namespace`` has been deprecated.
|
||||
|
||||
See the documentation for ``pytest_namespace`` hook for suggestions on how to deal
|
||||
with this in plugins which use this functionality.
|
||||
|
||||
|
||||
- `#3661 <https://github.com/pytest-dev/pytest/issues/3661>`_: Calling a fixture function directly, as opposed to request them in a test function, now issues a ``RemovedInPytest4Warning``. It will be changed into an error in pytest ``4.0``.
|
||||
|
||||
This is a great source of confusion to new users, which will often call the fixture functions and request them from test functions interchangeably, which breaks the fixture resolution model.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#2283 <https://github.com/pytest-dev/pytest/issues/2283>`_: New ``package`` fixture scope: fixtures are finalized when the last test of a *package* finishes. This feature is considered **experimental**, so use it sparingly.
|
||||
|
||||
|
||||
- `#3576 <https://github.com/pytest-dev/pytest/issues/3576>`_: ``Node.add_marker`` now supports an ``append=True/False`` parameter to determine whether the mark comes last (default) or first.
|
||||
|
||||
|
||||
- `#3579 <https://github.com/pytest-dev/pytest/issues/3579>`_: Fixture ``caplog`` now has a ``messages`` property, providing convenient access to the format-interpolated log messages without the extra data provided by the formatter/handler.
|
||||
|
||||
|
||||
- `#3610 <https://github.com/pytest-dev/pytest/issues/3610>`_: New ``--trace`` option to enter the debugger at the start of a test.
|
||||
|
||||
|
||||
- `#3623 <https://github.com/pytest-dev/pytest/issues/3623>`_: Introduce ``pytester.copy_example`` as helper to do acceptance tests against examples from the project.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#2220 <https://github.com/pytest-dev/pytest/issues/2220>`_: Fix a bug where fixtures overridden by direct parameters (for example parametrization) were being instantiated even if they were not being used by a test.
|
||||
|
||||
|
||||
- `#3695 <https://github.com/pytest-dev/pytest/issues/3695>`_: Fix ``ApproxNumpy`` initialisation argument mixup, ``abs`` and ``rel`` tolerances were flipped causing strange comparsion results.
|
||||
Add tests to check ``abs`` and ``rel`` tolerances for ``np.array`` and test for expecting ``nan`` with ``np.array()``
|
||||
|
||||
|
||||
- `#980 <https://github.com/pytest-dev/pytest/issues/980>`_: Fix truncated locals output in verbose mode.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3295 <https://github.com/pytest-dev/pytest/issues/3295>`_: Correct the usage documentation of ``--last-failed-no-failures`` by adding the missing ``--last-failed`` argument in the presented examples, because they are misleading and lead to think that the missing argument is not needed.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#3519 <https://github.com/pytest-dev/pytest/issues/3519>`_: Now a ``README.md`` file is created in ``.pytest_cache`` to make it clear why the directory exists.
|
||||
|
||||
|
||||
pytest 3.6.4 (2018-07-28)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Invoke pytest using ``-mpytest`` so ``sys.path`` does not get polluted by packages installed in ``site-packages``. (`#742 <https://github.com/pytest-dev/pytest/issues/742>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Use ``smtp_connection`` instead of ``smtp`` in fixtures documentation to avoid possible confusion. (`#3592 <https://github.com/pytest-dev/pytest/issues/3592>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Remove obsolete ``__future__`` imports. (`#2319 <https://github.com/pytest-dev/pytest/issues/2319>`_)
|
||||
|
||||
- Add CITATION to provide information on how to formally cite pytest. (`#3402 <https://github.com/pytest-dev/pytest/issues/3402>`_)
|
||||
|
||||
- Replace broken type annotations with type comments. (`#3635 <https://github.com/pytest-dev/pytest/issues/3635>`_)
|
||||
|
||||
- Pin ``pluggy`` to ``<0.8``. (`#3727 <https://github.com/pytest-dev/pytest/issues/3727>`_)
|
||||
|
||||
|
||||
pytest 3.6.3 (2018-07-04)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fix ``ImportWarning`` triggered by explicit relative imports in
|
||||
assertion-rewritten package modules. (`#3061
|
||||
<https://github.com/pytest-dev/pytest/issues/3061>`_)
|
||||
|
||||
- Fix error in ``pytest.approx`` when dealing with 0-dimension numpy
|
||||
arrays. (`#3593 <https://github.com/pytest-dev/pytest/issues/3593>`_)
|
||||
|
||||
- No longer raise ``ValueError`` when using the ``get_marker`` API. (`#3605
|
||||
<https://github.com/pytest-dev/pytest/issues/3605>`_)
|
||||
|
||||
- Fix problem where log messages with non-ascii characters would not
|
||||
appear in the output log file.
|
||||
(`#3630 <https://github.com/pytest-dev/pytest/issues/3630>`_)
|
||||
|
||||
- No longer raise ``AttributeError`` when legacy marks can't be stored in
|
||||
functions. (`#3631 <https://github.com/pytest-dev/pytest/issues/3631>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- The description above the example for ``@pytest.mark.skipif`` now better
|
||||
matches the code. (`#3611
|
||||
<https://github.com/pytest-dev/pytest/issues/3611>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Internal refactoring: removed unused ``CallSpec2tox ._globalid_args``
|
||||
attribute and ``metafunc`` parameter from ``CallSpec2.copy()``. (`#3598
|
||||
<https://github.com/pytest-dev/pytest/issues/3598>`_)
|
||||
|
||||
- Silence usage of ``reduce`` warning in Python 2 (`#3609
|
||||
<https://github.com/pytest-dev/pytest/issues/3609>`_)
|
||||
|
||||
- Fix usage of ``attr.ib`` deprecated ``convert`` parameter. (`#3653
|
||||
<https://github.com/pytest-dev/pytest/issues/3653>`_)
|
||||
|
||||
|
||||
pytest 3.6.2 (2018-06-20)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -26,7 +235,7 @@ Bug Fixes
|
||||
raises an exception. (`#3569
|
||||
<https://github.com/pytest-dev/pytest/issues/3569>`_)
|
||||
|
||||
- Fix encoding error with `print` statements in doctests (`#3583
|
||||
- Fix encoding error with ``print`` statements in doctests (`#3583
|
||||
<https://github.com/pytest-dev/pytest/issues/3583>`_)
|
||||
|
||||
|
||||
@@ -54,7 +263,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3567>`_)
|
||||
|
||||
|
||||
Pytest 3.6.1 (2018-06-05)
|
||||
pytest 3.6.1 (2018-06-05)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -98,7 +307,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3529>`_)
|
||||
|
||||
|
||||
Pytest 3.6.0 (2018-05-23)
|
||||
pytest 3.6.0 (2018-05-23)
|
||||
=========================
|
||||
|
||||
Features
|
||||
@@ -184,7 +393,7 @@ Trivial/Internal Changes
|
||||
3.7 or newer. (`#3497 <https://github.com/pytest-dev/pytest/issues/3497>`_)
|
||||
|
||||
|
||||
Pytest 3.5.1 (2018-04-23)
|
||||
pytest 3.5.1 (2018-04-23)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -236,7 +445,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3398>`_)
|
||||
|
||||
|
||||
Pytest 3.5.0 (2018-03-21)
|
||||
pytest 3.5.0 (2018-03-21)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
@@ -299,7 +508,7 @@ Features
|
||||
``pytest_runtest_logfinish`` hooks when live logs are enabled. (`#3189
|
||||
<https://github.com/pytest-dev/pytest/issues/3189>`_)
|
||||
|
||||
- Passing `--log-cli-level` in the command-line now automatically activates
|
||||
- Passing ``--log-cli-level`` in the command-line now automatically activates
|
||||
live logging. (`#3190 <https://github.com/pytest-dev/pytest/issues/3190>`_)
|
||||
|
||||
- Add command line option ``--deselect`` to allow deselection of individual
|
||||
@@ -388,7 +597,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3308>`_)
|
||||
|
||||
|
||||
Pytest 3.4.2 (2018-03-04)
|
||||
pytest 3.4.2 (2018-03-04)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -425,7 +634,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3259>`_)
|
||||
|
||||
|
||||
Pytest 3.4.1 (2018-02-20)
|
||||
pytest 3.4.1 (2018-02-20)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -486,7 +695,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/985>`_)
|
||||
|
||||
|
||||
Pytest 3.4.0 (2018-01-30)
|
||||
pytest 3.4.0 (2018-01-30)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
@@ -618,7 +827,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3129>`_)
|
||||
|
||||
|
||||
Pytest 3.3.2 (2017-12-25)
|
||||
pytest 3.3.2 (2017-12-25)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -651,11 +860,11 @@ Trivial/Internal Changes
|
||||
- Code cleanup. (`#3015 <https://github.com/pytest-dev/pytest/issues/3015>`_,
|
||||
`#3021 <https://github.com/pytest-dev/pytest/issues/3021>`_)
|
||||
|
||||
- Clean up code by replacing imports and references of `_ast` to `ast`. (`#3018
|
||||
<https://github.com/pytest-dev/pytest/issues/3018>`_)
|
||||
- Clean up code by replacing imports and references of ``_ast`` to ``ast``.
|
||||
(`#3018 <https://github.com/pytest-dev/pytest/issues/3018>`_)
|
||||
|
||||
|
||||
Pytest 3.3.1 (2017-12-05)
|
||||
pytest 3.3.1 (2017-12-05)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -697,13 +906,13 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/2949>`_)
|
||||
|
||||
|
||||
Pytest 3.3.0 (2017-11-23)
|
||||
pytest 3.3.0 (2017-11-23)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- Pytest no longer supports Python **2.6** and **3.3**. Those Python versions
|
||||
- pytest no longer supports Python **2.6** and **3.3**. Those Python versions
|
||||
are EOL for some time now and incur maintenance and compatibility costs on
|
||||
the pytest core team, and following up with the rest of the community we
|
||||
decided that they will no longer be supported starting on this version. Users
|
||||
@@ -757,7 +966,7 @@ Features
|
||||
- Match ``warns`` signature to ``raises`` by adding ``match`` keyword. (`#2708
|
||||
<https://github.com/pytest-dev/pytest/issues/2708>`_)
|
||||
|
||||
- Pytest now captures and displays output from the standard ``logging`` module.
|
||||
- pytest now captures and displays output from the standard ``logging`` module.
|
||||
The user can control the logging level to be captured by specifying options
|
||||
in ``pytest.ini``, the command line and also during individual tests using
|
||||
markers. Also, a ``caplog`` fixture is available that enables users to test
|
||||
@@ -822,7 +1031,7 @@ Bug Fixes
|
||||
avoids a number of potential problems. (`#2751
|
||||
<https://github.com/pytest-dev/pytest/issues/2751>`_)
|
||||
|
||||
- Pytest no longer complains about warnings with unicode messages being
|
||||
- pytest no longer complains about warnings with unicode messages being
|
||||
non-ascii compatible even for ascii-compatible messages. As a result of this,
|
||||
warnings with unicode messages are converted first to an ascii representation
|
||||
for safety. (`#2809 <https://github.com/pytest-dev/pytest/issues/2809>`_)
|
||||
@@ -874,7 +1083,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/2922>`_)
|
||||
|
||||
|
||||
Pytest 3.2.5 (2017-11-15)
|
||||
pytest 3.2.5 (2017-11-15)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -885,7 +1094,7 @@ Bug Fixes
|
||||
<https://github.com/pytest-dev/pytest/issues/2926>`_)
|
||||
|
||||
|
||||
Pytest 3.2.4 (2017-11-13)
|
||||
pytest 3.2.4 (2017-11-13)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -934,7 +1143,7 @@ Improved Documentation
|
||||
<https://github.com/pytest-dev/pytest/issues/911>`_)
|
||||
|
||||
|
||||
Pytest 3.2.3 (2017-10-03)
|
||||
pytest 3.2.3 (2017-10-03)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -974,13 +1183,13 @@ Trivial/Internal Changes
|
||||
(`#2765 <https://github.com/pytest-dev/pytest/issues/2765>`_)
|
||||
|
||||
|
||||
Pytest 3.2.2 (2017-09-06)
|
||||
pytest 3.2.2 (2017-09-06)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Calling the deprecated `request.getfuncargvalue()` now shows the source of
|
||||
- Calling the deprecated ``request.getfuncargvalue()`` now shows the source of
|
||||
the call. (`#2681 <https://github.com/pytest-dev/pytest/issues/2681>`_)
|
||||
|
||||
- Allow tests declared as ``@staticmethod`` to use fixtures. (`#2699
|
||||
@@ -1002,10 +1211,10 @@ Improved Documentation
|
||||
``pytest.mark.MARKER_NAME.__call__`` (`#2604
|
||||
<https://github.com/pytest-dev/pytest/issues/2604>`_)
|
||||
|
||||
- In one of the simple examples, use `pytest_collection_modifyitems()` to skip
|
||||
- In one of the simple examples, use ``pytest_collection_modifyitems()`` to skip
|
||||
tests based on a command-line option, allowing its sharing while preventing a
|
||||
user error when acessing `pytest.config` before the argument parsing. (`#2653
|
||||
<https://github.com/pytest-dev/pytest/issues/2653>`_)
|
||||
user error when acessing ``pytest.config`` before the argument parsing.
|
||||
(`#2653 <https://github.com/pytest-dev/pytest/issues/2653>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
@@ -1021,7 +1230,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/2739>`_)
|
||||
|
||||
|
||||
Pytest 3.2.1 (2017-08-08)
|
||||
pytest 3.2.1 (2017-08-08)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -1051,7 +1260,7 @@ Improved Documentation
|
||||
<https://github.com/pytest-dev/pytest/issues/2626>`_)
|
||||
|
||||
|
||||
Pytest 3.2.0 (2017-07-30)
|
||||
pytest 3.2.0 (2017-07-30)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
@@ -1083,7 +1292,7 @@ Features
|
||||
from parent classes or modules. (`#2516 <https://github.com/pytest-
|
||||
dev/pytest/issues/2516>`_)
|
||||
|
||||
- Collection ignores local virtualenvs by default; `--collect-in-virtualenv`
|
||||
- Collection ignores local virtualenvs by default; ``--collect-in-virtualenv``
|
||||
overrides this behavior. (`#2518 <https://github.com/pytest-
|
||||
dev/pytest/issues/2518>`_)
|
||||
|
||||
@@ -1217,7 +1426,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/2620>`_)
|
||||
|
||||
|
||||
Pytest 3.1.3 (2017-07-03)
|
||||
pytest 3.1.3 (2017-07-03)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -1263,7 +1472,7 @@ Trivial/Internal Changes
|
||||
(`#2499 <https://github.com/pytest-dev/pytest/issues/2499>`_)
|
||||
|
||||
|
||||
Pytest 3.1.2 (2017-06-08)
|
||||
pytest 3.1.2 (2017-06-08)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -1295,7 +1504,7 @@ Improved Documentation
|
||||
and improve overall flow of the ``skipping`` docs. (#810)
|
||||
|
||||
|
||||
Pytest 3.1.1 (2017-05-30)
|
||||
pytest 3.1.1 (2017-05-30)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
|
||||
16
CITATION
Normal file
16
CITATION
Normal file
@@ -0,0 +1,16 @@
|
||||
NOTE: Change "x.y" by the version you use. If you are unsure about which version
|
||||
you are using run: `pip show pytest`.
|
||||
|
||||
Text:
|
||||
|
||||
[pytest] pytest x.y, 2004
|
||||
Krekel et al., https://github.com/pytest-dev/pytest
|
||||
|
||||
BibTeX:
|
||||
|
||||
@misc{pytestx.y,
|
||||
title = {pytest x.y},
|
||||
author = {Krekel, Holger and Oliveira, Bruno and Pfannschmidt, Ronny and Bruynooghe, Floris and Laugher, Brianna and Bruhin, Florian},
|
||||
year = {2004},
|
||||
url = {https://github.com/pytest-dev/pytest},
|
||||
}
|
||||
@@ -10,10 +10,6 @@ taking a lot of time to make a new one.
|
||||
pytest releases must be prepared on **Linux** because the docs and examples expect
|
||||
to be executed in that platform.
|
||||
|
||||
#. Install development dependencies in a virtual environment with::
|
||||
|
||||
pip3 install -U -r tasks/requirements.txt
|
||||
|
||||
#. Create a branch ``release-X.Y.Z`` with the version for the release.
|
||||
|
||||
* **patch releases**: from the latest ``master``;
|
||||
@@ -22,9 +18,11 @@ taking a lot of time to make a new one.
|
||||
|
||||
Ensure your are in a clean work tree.
|
||||
|
||||
#. Generate docs, changelog, announcements and a **local** tag::
|
||||
#. Using ``tox``, generate docs, changelog, announcements::
|
||||
|
||||
invoke generate.pre-release <VERSION>
|
||||
$ tox -e release -- <VERSION>
|
||||
|
||||
This will generate a commit with all the changes ready for pushing.
|
||||
|
||||
#. Open a PR for this branch targeting ``master``.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
:align: center
|
||||
:alt: pytest
|
||||
|
||||
|
||||
------
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/pytest.svg
|
||||
@@ -109,7 +110,7 @@ Consult the `Changelog <http://docs.pytest.org/en/latest/changelog.html>`__ page
|
||||
License
|
||||
-------
|
||||
|
||||
Copyright Holger Krekel and others, 2004-2017.
|
||||
Copyright Holger Krekel and others, 2004-2018.
|
||||
|
||||
Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ environment:
|
||||
- TOXENV: "py34"
|
||||
- TOXENV: "py35"
|
||||
- TOXENV: "py36"
|
||||
- TOXENV: "pypy"
|
||||
- TOXENV: "py37"
|
||||
# - TOXENV: "pypy" reenable when we are able to provide a scandir wheel or build scandir
|
||||
- TOXENV: "py27-pexpect"
|
||||
- TOXENV: "py27-xdist"
|
||||
- TOXENV: "py27-trial"
|
||||
@@ -27,7 +28,7 @@ environment:
|
||||
- TOXENV: "py36-pluggymaster"
|
||||
- TOXENV: "py27-nobyte"
|
||||
- TOXENV: "doctesting"
|
||||
- TOXENV: "py35-freeze"
|
||||
- TOXENV: "py36-freeze"
|
||||
- TOXENV: "docs"
|
||||
|
||||
install:
|
||||
@@ -46,3 +47,7 @@ test_script:
|
||||
cache:
|
||||
- '%LOCALAPPDATA%\pip\cache'
|
||||
- '%USERPROFILE%\.cache\pre-commit'
|
||||
|
||||
# We don't deploy anything on tags with AppVeyor, we use Travis instead, so we
|
||||
# might as well save resources
|
||||
skip_tags: true
|
||||
|
||||
@@ -26,7 +26,7 @@ changelog using that instead.
|
||||
|
||||
If you are not sure what issue type to use, don't hesitate to ask in your PR.
|
||||
|
||||
Note that the ``towncrier`` tool will automatically
|
||||
reflow your text, so it will work best if you stick to a single paragraph, but multiple sentences and links are OK
|
||||
and encouraged. You can install ``towncrier`` and then run ``towncrier --draft``
|
||||
``towncrier`` preserves multiple paragraphs and formatting (code blocks, lists, and so on), but for entries
|
||||
other than ``features`` it is usually better to stick to a single paragraph to keep it concise. You can install
|
||||
``towncrier`` and then run ``towncrier --draft``
|
||||
if you want to get a preview of how your change will look in the final release notes.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% if definitions[category]['showcontent'] %}
|
||||
{% for text, values in sections[section][category]|dictsort(by='value') %}
|
||||
{% set issue_joiner = joiner(', ') %}
|
||||
- {{ text }}{% if category != 'vendor' %} ({% for value in values|sort %}{{ issue_joiner() }}`{{ value }} <https://github.com/pytest-dev/pytest/issues/{{ value[1:] }}>`_{% endfor %}){% endif %}
|
||||
- {% for value in values|sort %}{{ issue_joiner() }}`{{ value }} <https://github.com/pytest-dev/pytest/issues/{{ value[1:] }}>`_{% endfor %}: {{ text }}
|
||||
|
||||
|
||||
{% endfor %}
|
||||
|
||||
@@ -6,6 +6,11 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-3.7.2
|
||||
release-3.7.1
|
||||
release-3.7.0
|
||||
release-3.6.4
|
||||
release-3.6.3
|
||||
release-3.6.2
|
||||
release-3.6.1
|
||||
release-3.6.0
|
||||
|
||||
@@ -23,7 +23,7 @@ a full list of details. A few feature highlights:
|
||||
called if the corresponding setup method succeeded.
|
||||
|
||||
- integrate tab-completion on command line options if you
|
||||
have `argcomplete <http://pypi.python.org/pypi/argcomplete>`_
|
||||
have `argcomplete <https://pypi.org/project/argcomplete/>`_
|
||||
configured.
|
||||
|
||||
- allow boolean expression directly with skipif/xfail
|
||||
|
||||
@@ -124,7 +124,7 @@ The py.test Development Team
|
||||
Thanks `@biern`_ for the PR.
|
||||
|
||||
* Fix `traceback style docs`_ to describe all of the available options
|
||||
(auto/long/short/line/native/no), with `auto` being the default since v2.6.
|
||||
(auto/long/short/line/native/no), with ``auto`` being the default since v2.6.
|
||||
Thanks `@hackebrot`_ for the PR.
|
||||
|
||||
* Fix (`#1422`_): junit record_xml_property doesn't allow multiple records
|
||||
|
||||
28
doc/en/announce/release-3.6.3.rst
Normal file
28
doc/en/announce/release-3.6.3.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pytest-3.6.3
|
||||
=======================================
|
||||
|
||||
pytest 3.6.3 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* AdamEr8
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Jean-Paul Calderone
|
||||
* Jon Dufresne
|
||||
* Marcelo Duarte Trevisani
|
||||
* Ondřej Súkup
|
||||
* Ronny Pfannschmidt
|
||||
* T.E.A de Souza
|
||||
* Victor
|
||||
* victor
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
24
doc/en/announce/release-3.6.4.rst
Normal file
24
doc/en/announce/release-3.6.4.rst
Normal file
@@ -0,0 +1,24 @@
|
||||
pytest-3.6.4
|
||||
=======================================
|
||||
|
||||
pytest 3.6.4 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 http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bernhard M. Wiedemann
|
||||
* Bruno Oliveira
|
||||
* Drew
|
||||
* E Hershey
|
||||
* Hugo Martins
|
||||
* Vlad Shcherbina
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
41
doc/en/announce/release-3.7.0.rst
Normal file
41
doc/en/announce/release-3.7.0.rst
Normal file
@@ -0,0 +1,41 @@
|
||||
pytest-3.7.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.7.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 2000 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
This release contains a number of bugs fixes and improvements, so users are encouraged
|
||||
to take a look at the CHANGELOG:
|
||||
|
||||
http://doc.pytest.org/en/latest/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
http://docs.pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Alan
|
||||
* Alan Brammer
|
||||
* Ammar Najjar
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Jeffrey Rackauckas
|
||||
* Kale Kundert
|
||||
* Ronny Pfannschmidt
|
||||
* Serhii Mozghovyi
|
||||
* Tadek Teleżyński
|
||||
* Wil Cooley
|
||||
* abrammer
|
||||
* avirlrma
|
||||
* turturica
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
21
doc/en/announce/release-3.7.1.rst
Normal file
21
doc/en/announce/release-3.7.1.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
pytest-3.7.1
|
||||
=======================================
|
||||
|
||||
pytest 3.7.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 http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Kale Kundert
|
||||
* Ronny Pfannschmidt
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
25
doc/en/announce/release-3.7.2.rst
Normal file
25
doc/en/announce/release-3.7.2.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-3.7.2
|
||||
=======================================
|
||||
|
||||
pytest 3.7.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 http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Josh Holland
|
||||
* Ronny Pfannschmidt
|
||||
* Sankt Petersbug
|
||||
* Wes Thomas
|
||||
* turturica
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -90,7 +90,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
monkeypatch.setitem(mapping, name, value)
|
||||
monkeypatch.delitem(obj, name, raising=True)
|
||||
monkeypatch.setenv(name, value, prepend=False)
|
||||
monkeypatch.delenv(name, value, raising=True)
|
||||
monkeypatch.delenv(name, raising=True)
|
||||
monkeypatch.syspath_prepend(path)
|
||||
monkeypatch.chdir(path)
|
||||
|
||||
|
||||
@@ -162,8 +162,8 @@ When no tests failed in the last run, or when no cached ``lastfailed`` data was
|
||||
found, ``pytest`` can be configured either to run all of the tests or no tests,
|
||||
using the ``--last-failed-no-failures`` option, which takes one of the following values::
|
||||
|
||||
pytest --last-failed-no-failures all # run all tests (default behavior)
|
||||
pytest --last-failed-no-failures none # run no tests and exit
|
||||
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # run no tests and exit
|
||||
|
||||
The new config.cache object
|
||||
--------------------------------
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
|
||||
.. _changelog:
|
||||
|
||||
Changelog history
|
||||
=================================
|
||||
|
||||
.. include:: ../../CHANGELOG.rst
|
||||
|
||||
@@ -65,7 +65,7 @@ master_doc = "contents"
|
||||
# General information about the project.
|
||||
project = u"pytest"
|
||||
year = datetime.datetime.utcnow().year
|
||||
copyright = u"2015–{} , holger krekel and pytest-dev team".format(year)
|
||||
copyright = u"2015–2018 , holger krekel and pytest-dev team"
|
||||
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
|
||||
@@ -139,7 +139,7 @@ line options while the environment is in use::
|
||||
|
||||
Here's how the command-line is built in the presence of ``addopts`` or the environment variable::
|
||||
|
||||
<pytest.ini:addopts> $PYTEST_ADDOTPS <extra command-line arguments>
|
||||
<pytest.ini:addopts> $PYTEST_ADDOPTS <extra command-line arguments>
|
||||
|
||||
So if the user executes in the command-line::
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ avoid creating labels just for the sake of creating them.
|
||||
|
||||
Each label should include a description in the GitHub's interface stating its purpose.
|
||||
|
||||
Labels are managed using `labels <https://github.com/hackebrot/labels>`_. All the labels in the repository
|
||||
are kept in ``.github/labels.toml``, so any changes should be via PRs to that file.
|
||||
After a PR is accepted and merged, one of the maintainers must manually synchronize the labels file with the
|
||||
GitHub repository.
|
||||
|
||||
Temporary labels
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -25,9 +25,7 @@ def pytest_generate_tests(metafunc):
|
||||
|
||||
|
||||
class TestFailing(object):
|
||||
|
||||
def test_simple(self):
|
||||
|
||||
def f():
|
||||
return 42
|
||||
|
||||
@@ -40,7 +38,6 @@ class TestFailing(object):
|
||||
otherfunc_multi(42, 6 * 9)
|
||||
|
||||
def test_not(self):
|
||||
|
||||
def f():
|
||||
return 42
|
||||
|
||||
@@ -48,7 +45,6 @@ class TestFailing(object):
|
||||
|
||||
|
||||
class TestSpecialisedExplanations(object):
|
||||
|
||||
def test_eq_text(self):
|
||||
assert "spam" == "eggs"
|
||||
|
||||
@@ -106,7 +102,6 @@ class TestSpecialisedExplanations(object):
|
||||
|
||||
|
||||
def test_attribute():
|
||||
|
||||
class Foo(object):
|
||||
b = 1
|
||||
|
||||
@@ -115,7 +110,6 @@ def test_attribute():
|
||||
|
||||
|
||||
def test_attribute_instance():
|
||||
|
||||
class Foo(object):
|
||||
b = 1
|
||||
|
||||
@@ -123,9 +117,7 @@ def test_attribute_instance():
|
||||
|
||||
|
||||
def test_attribute_failure():
|
||||
|
||||
class Foo(object):
|
||||
|
||||
def _get_b(self):
|
||||
raise Exception("Failed to get attrib")
|
||||
|
||||
@@ -136,7 +128,6 @@ def test_attribute_failure():
|
||||
|
||||
|
||||
def test_attribute_multiple():
|
||||
|
||||
class Foo(object):
|
||||
b = 1
|
||||
|
||||
@@ -151,7 +142,6 @@ def globf(x):
|
||||
|
||||
|
||||
class TestRaises(object):
|
||||
|
||||
def test_raises(self):
|
||||
s = "qwe" # NOQA
|
||||
raises(TypeError, "int(s)")
|
||||
@@ -193,9 +183,7 @@ def test_dynamic_compile_shows_nicely():
|
||||
|
||||
|
||||
class TestMoreErrors(object):
|
||||
|
||||
def test_complex_error(self):
|
||||
|
||||
def f():
|
||||
return 44
|
||||
|
||||
@@ -218,7 +206,6 @@ class TestMoreErrors(object):
|
||||
assert s.startswith(g)
|
||||
|
||||
def test_startswith_nested(self):
|
||||
|
||||
def f():
|
||||
return "123"
|
||||
|
||||
@@ -246,9 +233,7 @@ class TestMoreErrors(object):
|
||||
|
||||
|
||||
class TestCustomAssertMsg(object):
|
||||
|
||||
def test_single_line(self):
|
||||
|
||||
class A(object):
|
||||
a = 1
|
||||
|
||||
@@ -256,7 +241,6 @@ class TestCustomAssertMsg(object):
|
||||
assert A.a == b, "A.a appears not to be b"
|
||||
|
||||
def test_multiline(self):
|
||||
|
||||
class A(object):
|
||||
a = 1
|
||||
|
||||
@@ -266,7 +250,6 @@ class TestCustomAssertMsg(object):
|
||||
), "A.a appears not to be b\n" "or does not appear to be b\none of those"
|
||||
|
||||
def test_custom_repr(self):
|
||||
|
||||
class JSON(object):
|
||||
a = 1
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import py
|
||||
|
||||
failure_demo = py.path.local(__file__).dirpath("failure_demo.py")
|
||||
pytest_plugins = "pytester",
|
||||
pytest_plugins = ("pytester",)
|
||||
|
||||
|
||||
def test_failure_demo_fails_properly(testdir):
|
||||
|
||||
@@ -3,7 +3,6 @@ def setup_module(module):
|
||||
|
||||
|
||||
class TestStateFullThing(object):
|
||||
|
||||
def setup_class(cls):
|
||||
cls.classcount += 1
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ def setup(request):
|
||||
|
||||
|
||||
class CostlySetup(object):
|
||||
|
||||
def __init__(self):
|
||||
import time
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ You can then restrict a test run to only run tests marked with ``webtest``::
|
||||
|
||||
$ pytest -v -m webtest
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items / 3 deselected
|
||||
@@ -44,7 +44,7 @@ Or the inverse, running all tests except the webtest ones::
|
||||
|
||||
$ pytest -v -m "not webtest"
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items / 1 deselected
|
||||
@@ -64,7 +64,7 @@ tests based on their module, class, method, or function name::
|
||||
|
||||
$ pytest -v test_server.py::TestClass::test_method
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 1 item
|
||||
@@ -77,7 +77,7 @@ You can also select on the class::
|
||||
|
||||
$ pytest -v test_server.py::TestClass
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 1 item
|
||||
@@ -90,7 +90,7 @@ Or select multiple nodes::
|
||||
|
||||
$ pytest -v test_server.py::TestClass test_server.py::test_send_http
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 2 items
|
||||
@@ -128,7 +128,7 @@ select tests based on their names::
|
||||
|
||||
$ pytest -v -k http # running with the above defined example module
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items / 3 deselected
|
||||
@@ -141,7 +141,7 @@ And you can also run all tests except the ones that match the keyword::
|
||||
|
||||
$ pytest -k "not send_http" -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items / 1 deselected
|
||||
@@ -156,7 +156,7 @@ Or to select "http" and "quick" tests::
|
||||
|
||||
$ pytest -k "http or quick" -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items / 2 deselected
|
||||
@@ -200,6 +200,8 @@ You can ask which markers exist for your test suite - the list includes our just
|
||||
$ pytest --markers
|
||||
@pytest.mark.webtest: mark a test as a webtest.
|
||||
|
||||
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see http://pytest.org/latest/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.
|
||||
|
||||
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html
|
||||
@@ -299,10 +301,10 @@ Skip and xfail marks can also be applied in this way, see :ref:`skip/xfail with
|
||||
.. note::
|
||||
|
||||
If the data you are parametrizing happen to be single callables, you need to be careful
|
||||
when marking these items. `pytest.mark.xfail(my_func)` won't work because it's also the
|
||||
when marking these items. ``pytest.mark.xfail(my_func)`` won't work because it's also the
|
||||
signature of a function being decorated. To resolve this ambiguity, you need to pass a
|
||||
reason argument:
|
||||
`pytest.mark.xfail(func_bar, reason="Issue#7")`.
|
||||
``pytest.mark.xfail(func_bar, reason="Issue#7")``.
|
||||
|
||||
|
||||
.. _`adding a custom marker from a plugin`:
|
||||
@@ -374,6 +376,8 @@ The ``--markers`` option always gives you a list of available markers::
|
||||
$ pytest --markers
|
||||
@pytest.mark.env(name): mark test to run only on named environment
|
||||
|
||||
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see http://pytest.org/latest/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.
|
||||
|
||||
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html
|
||||
|
||||
@@ -21,7 +21,6 @@ def python2(request, python1):
|
||||
|
||||
|
||||
class Python(object):
|
||||
|
||||
def __init__(self, version, picklefile):
|
||||
self.pythonpath = py.path.local.sysfind(version)
|
||||
if not self.pythonpath:
|
||||
|
||||
@@ -59,7 +59,7 @@ consulted when reporting in ``verbose`` mode::
|
||||
|
||||
nonpython $ pytest -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
|
||||
collecting ... collected 2 items
|
||||
@@ -84,8 +84,9 @@ interesting to just look at the collection tree::
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
|
||||
collected 2 items
|
||||
<YamlFile 'test_simple.yml'>
|
||||
<YamlItem 'hello'>
|
||||
<YamlItem 'ok'>
|
||||
<Package '$REGENDOC_TMPDIR/nonpython'>
|
||||
<YamlFile 'test_simple.yml'>
|
||||
<YamlItem 'hello'>
|
||||
<YamlItem 'ok'>
|
||||
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
@@ -9,7 +9,6 @@ def pytest_collect_file(parent, path):
|
||||
|
||||
|
||||
class YamlFile(pytest.File):
|
||||
|
||||
def collect(self):
|
||||
import yaml # we need a yaml parser, e.g. PyYAML
|
||||
|
||||
@@ -19,7 +18,6 @@ class YamlFile(pytest.File):
|
||||
|
||||
|
||||
class YamlItem(pytest.Item):
|
||||
|
||||
def __init__(self, name, parent, spec):
|
||||
super(YamlItem, self).__init__(name, parent)
|
||||
self.spec = spec
|
||||
|
||||
@@ -411,8 +411,10 @@ is to be run with different sets of arguments for its three arguments:
|
||||
Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize)::
|
||||
|
||||
. $ pytest -rs -q multipython.py
|
||||
........................... [100%]
|
||||
27 passed in 0.12 seconds
|
||||
...sss...sssssssss...sss... [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIP [15] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.4' not found
|
||||
12 passed, 15 skipped in 0.12 seconds
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
--------------------------------------------------------------------
|
||||
|
||||
@@ -5,7 +5,6 @@ py3 = sys.version_info[0] >= 3
|
||||
|
||||
|
||||
class DummyCollector(pytest.collect.File):
|
||||
|
||||
def collect(self):
|
||||
return []
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ def test_function():
|
||||
|
||||
|
||||
class TestClass(object):
|
||||
|
||||
def test_method(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ get on the terminal - we are working on that)::
|
||||
self = <failure_demo.TestFailing object at 0xdeadbeef>
|
||||
|
||||
def test_simple(self):
|
||||
|
||||
def f():
|
||||
return 42
|
||||
|
||||
@@ -44,7 +43,7 @@ get on the terminal - we are working on that)::
|
||||
E + where 42 = <function TestFailing.test_simple.<locals>.f at 0xdeadbeef>()
|
||||
E + and 43 = <function TestFailing.test_simple.<locals>.g at 0xdeadbeef>()
|
||||
|
||||
failure_demo.py:37: AssertionError
|
||||
failure_demo.py:35: AssertionError
|
||||
____________________ TestFailing.test_simple_multiline _____________________
|
||||
|
||||
self = <failure_demo.TestFailing object at 0xdeadbeef>
|
||||
@@ -52,7 +51,7 @@ get on the terminal - we are working on that)::
|
||||
def test_simple_multiline(self):
|
||||
> otherfunc_multi(42, 6 * 9)
|
||||
|
||||
failure_demo.py:40:
|
||||
failure_demo.py:38:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
a = 42, b = 54
|
||||
@@ -67,7 +66,6 @@ get on the terminal - we are working on that)::
|
||||
self = <failure_demo.TestFailing object at 0xdeadbeef>
|
||||
|
||||
def test_not(self):
|
||||
|
||||
def f():
|
||||
return 42
|
||||
|
||||
@@ -75,7 +73,7 @@ get on the terminal - we are working on that)::
|
||||
E assert not 42
|
||||
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef>()
|
||||
|
||||
failure_demo.py:47: AssertionError
|
||||
failure_demo.py:44: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_text _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -86,7 +84,7 @@ get on the terminal - we are working on that)::
|
||||
E - spam
|
||||
E + eggs
|
||||
|
||||
failure_demo.py:53: AssertionError
|
||||
failure_demo.py:49: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -99,7 +97,7 @@ get on the terminal - we are working on that)::
|
||||
E + foo 2 bar
|
||||
E ? ^
|
||||
|
||||
failure_demo.py:56: AssertionError
|
||||
failure_demo.py:52: AssertionError
|
||||
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -112,7 +110,7 @@ get on the terminal - we are working on that)::
|
||||
E + eggs
|
||||
E bar
|
||||
|
||||
failure_demo.py:59: AssertionError
|
||||
failure_demo.py:55: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_long_text _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -129,7 +127,7 @@ get on the terminal - we are working on that)::
|
||||
E + 1111111111b222222222
|
||||
E ? ^
|
||||
|
||||
failure_demo.py:64: AssertionError
|
||||
failure_demo.py:60: AssertionError
|
||||
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -149,7 +147,7 @@ get on the terminal - we are working on that)::
|
||||
E
|
||||
E ...Full output truncated (7 lines hidden), use '-vv' to show
|
||||
|
||||
failure_demo.py:69: AssertionError
|
||||
failure_demo.py:65: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -160,7 +158,7 @@ get on the terminal - we are working on that)::
|
||||
E At index 2 diff: 2 != 3
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:72: AssertionError
|
||||
failure_demo.py:68: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -173,7 +171,7 @@ get on the terminal - we are working on that)::
|
||||
E At index 100 diff: 1 != 2
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:77: AssertionError
|
||||
failure_demo.py:73: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -191,7 +189,7 @@ get on the terminal - we are working on that)::
|
||||
E
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
|
||||
failure_demo.py:80: AssertionError
|
||||
failure_demo.py:76: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -209,7 +207,7 @@ get on the terminal - we are working on that)::
|
||||
E
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
|
||||
failure_demo.py:83: AssertionError
|
||||
failure_demo.py:79: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -220,7 +218,7 @@ get on the terminal - we are working on that)::
|
||||
E Right contains more items, first extra item: 3
|
||||
E Use -v to get the full diff
|
||||
|
||||
failure_demo.py:86: AssertionError
|
||||
failure_demo.py:82: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_in_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -229,7 +227,7 @@ get on the terminal - we are working on that)::
|
||||
> assert 1 in [0, 2, 3, 4, 5]
|
||||
E assert 1 in [0, 2, 3, 4, 5]
|
||||
|
||||
failure_demo.py:89: AssertionError
|
||||
failure_demo.py:85: AssertionError
|
||||
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -248,7 +246,7 @@ get on the terminal - we are working on that)::
|
||||
E
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
|
||||
failure_demo.py:93: AssertionError
|
||||
failure_demo.py:89: AssertionError
|
||||
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -261,7 +259,7 @@ get on the terminal - we are working on that)::
|
||||
E single foo line
|
||||
E ? +++
|
||||
|
||||
failure_demo.py:97: AssertionError
|
||||
failure_demo.py:93: AssertionError
|
||||
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -274,7 +272,7 @@ get on the terminal - we are working on that)::
|
||||
E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail
|
||||
E ? +++
|
||||
|
||||
failure_demo.py:101: AssertionError
|
||||
failure_demo.py:97: AssertionError
|
||||
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef>
|
||||
@@ -287,11 +285,10 @@ get on the terminal - we are working on that)::
|
||||
E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail
|
||||
E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
failure_demo.py:105: AssertionError
|
||||
failure_demo.py:101: AssertionError
|
||||
______________________________ test_attribute ______________________________
|
||||
|
||||
def test_attribute():
|
||||
|
||||
class Foo(object):
|
||||
b = 1
|
||||
|
||||
@@ -300,11 +297,10 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 == 2
|
||||
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef>.b
|
||||
|
||||
failure_demo.py:114: AssertionError
|
||||
failure_demo.py:109: AssertionError
|
||||
_________________________ test_attribute_instance __________________________
|
||||
|
||||
def test_attribute_instance():
|
||||
|
||||
class Foo(object):
|
||||
b = 1
|
||||
|
||||
@@ -313,13 +309,11 @@ get on the terminal - we are working on that)::
|
||||
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef>.b
|
||||
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
||||
|
||||
failure_demo.py:122: AssertionError
|
||||
failure_demo.py:116: AssertionError
|
||||
__________________________ test_attribute_failure __________________________
|
||||
|
||||
def test_attribute_failure():
|
||||
|
||||
class Foo(object):
|
||||
|
||||
def _get_b(self):
|
||||
raise Exception("Failed to get attrib")
|
||||
|
||||
@@ -328,7 +322,7 @@ get on the terminal - we are working on that)::
|
||||
i = Foo()
|
||||
> assert i.b == 2
|
||||
|
||||
failure_demo.py:135:
|
||||
failure_demo.py:127:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef>
|
||||
@@ -337,11 +331,10 @@ get on the terminal - we are working on that)::
|
||||
> raise Exception("Failed to get attrib")
|
||||
E Exception: Failed to get attrib
|
||||
|
||||
failure_demo.py:130: Exception
|
||||
failure_demo.py:122: Exception
|
||||
_________________________ test_attribute_multiple __________________________
|
||||
|
||||
def test_attribute_multiple():
|
||||
|
||||
class Foo(object):
|
||||
b = 1
|
||||
|
||||
@@ -355,7 +348,7 @@ get on the terminal - we are working on that)::
|
||||
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef>.b
|
||||
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
||||
|
||||
failure_demo.py:146: AssertionError
|
||||
failure_demo.py:137: AssertionError
|
||||
__________________________ TestRaises.test_raises __________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
@@ -364,13 +357,13 @@ get on the terminal - we are working on that)::
|
||||
s = "qwe" # NOQA
|
||||
> raises(TypeError, "int(s)")
|
||||
|
||||
failure_demo.py:157:
|
||||
failure_demo.py:147:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
> int(s)
|
||||
E ValueError: invalid literal for int() with base 10: 'qwe'
|
||||
|
||||
<0-codegen $PYTHON_PREFIX/lib/python3.5/site-packages/_pytest/python_api.py:634>:1: ValueError
|
||||
<0-codegen $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/python_api.py:682>:1: ValueError
|
||||
______________________ TestRaises.test_raises_doesnt _______________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
@@ -379,7 +372,7 @@ get on the terminal - we are working on that)::
|
||||
> raises(IOError, "int('3')")
|
||||
E Failed: DID NOT RAISE <class 'OSError'>
|
||||
|
||||
failure_demo.py:160: Failed
|
||||
failure_demo.py:150: Failed
|
||||
__________________________ TestRaises.test_raise ___________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
@@ -388,7 +381,7 @@ get on the terminal - we are working on that)::
|
||||
> raise ValueError("demo error")
|
||||
E ValueError: demo error
|
||||
|
||||
failure_demo.py:163: ValueError
|
||||
failure_demo.py:153: ValueError
|
||||
________________________ TestRaises.test_tupleerror ________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
@@ -397,7 +390,7 @@ get on the terminal - we are working on that)::
|
||||
> a, b = [1] # NOQA
|
||||
E ValueError: not enough values to unpack (expected 2, got 1)
|
||||
|
||||
failure_demo.py:166: ValueError
|
||||
failure_demo.py:156: ValueError
|
||||
______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
@@ -408,7 +401,7 @@ get on the terminal - we are working on that)::
|
||||
> a, b = items.pop()
|
||||
E TypeError: 'int' object is not iterable
|
||||
|
||||
failure_demo.py:171: TypeError
|
||||
failure_demo.py:161: TypeError
|
||||
--------------------------- Captured stdout call ---------------------------
|
||||
items is [1, 2, 3]
|
||||
________________________ TestRaises.test_some_error ________________________
|
||||
@@ -419,7 +412,7 @@ get on the terminal - we are working on that)::
|
||||
> if namenotexi: # NOQA
|
||||
E NameError: name 'namenotexi' is not defined
|
||||
|
||||
failure_demo.py:174: NameError
|
||||
failure_demo.py:164: NameError
|
||||
____________________ test_dynamic_compile_shows_nicely _____________________
|
||||
|
||||
def test_dynamic_compile_shows_nicely():
|
||||
@@ -434,20 +427,19 @@ get on the terminal - we are working on that)::
|
||||
sys.modules[name] = module
|
||||
> module.foo()
|
||||
|
||||
failure_demo.py:192:
|
||||
failure_demo.py:182:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
def foo():
|
||||
> assert 1 == 0
|
||||
E AssertionError
|
||||
|
||||
<2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:189>:2: AssertionError
|
||||
<2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:179>:2: AssertionError
|
||||
____________________ TestMoreErrors.test_complex_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
def test_complex_error(self):
|
||||
|
||||
def f():
|
||||
return 44
|
||||
|
||||
@@ -456,7 +448,7 @@ get on the terminal - we are working on that)::
|
||||
|
||||
> somefunc(f(), g())
|
||||
|
||||
failure_demo.py:205:
|
||||
failure_demo.py:193:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
failure_demo.py:11: in somefunc
|
||||
otherfunc(x, y)
|
||||
@@ -478,7 +470,7 @@ get on the terminal - we are working on that)::
|
||||
> a, b = items
|
||||
E ValueError: not enough values to unpack (expected 2, got 0)
|
||||
|
||||
failure_demo.py:209: ValueError
|
||||
failure_demo.py:197: ValueError
|
||||
____________________ TestMoreErrors.test_z2_type_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -488,7 +480,7 @@ get on the terminal - we are working on that)::
|
||||
> a, b = items
|
||||
E TypeError: 'int' object is not iterable
|
||||
|
||||
failure_demo.py:213: TypeError
|
||||
failure_demo.py:201: TypeError
|
||||
______________________ TestMoreErrors.test_startswith ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -501,13 +493,12 @@ get on the terminal - we are working on that)::
|
||||
E + where False = <built-in method startswith of str object at 0xdeadbeef>('456')
|
||||
E + where <built-in method startswith of str object at 0xdeadbeef> = '123'.startswith
|
||||
|
||||
failure_demo.py:218: AssertionError
|
||||
failure_demo.py:206: AssertionError
|
||||
__________________ TestMoreErrors.test_startswith_nested ___________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
|
||||
def test_startswith_nested(self):
|
||||
|
||||
def f():
|
||||
return "123"
|
||||
|
||||
@@ -521,7 +512,7 @@ get on the terminal - we are working on that)::
|
||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef>()
|
||||
|
||||
failure_demo.py:228: AssertionError
|
||||
failure_demo.py:215: AssertionError
|
||||
_____________________ TestMoreErrors.test_global_func ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -532,7 +523,7 @@ get on the terminal - we are working on that)::
|
||||
E + where False = isinstance(43, float)
|
||||
E + where 43 = globf(42)
|
||||
|
||||
failure_demo.py:231: AssertionError
|
||||
failure_demo.py:218: AssertionError
|
||||
_______________________ TestMoreErrors.test_instance _______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -543,7 +534,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 42 != 42
|
||||
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef>.x
|
||||
|
||||
failure_demo.py:235: AssertionError
|
||||
failure_demo.py:222: AssertionError
|
||||
_______________________ TestMoreErrors.test_compare ________________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -553,7 +544,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 11 < 5
|
||||
E + where 11 = globf(10)
|
||||
|
||||
failure_demo.py:238: AssertionError
|
||||
failure_demo.py:225: AssertionError
|
||||
_____________________ TestMoreErrors.test_try_finally ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
|
||||
@@ -564,13 +555,12 @@ get on the terminal - we are working on that)::
|
||||
> assert x == 0
|
||||
E assert 1 == 0
|
||||
|
||||
failure_demo.py:243: AssertionError
|
||||
failure_demo.py:230: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_single_line ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
|
||||
|
||||
def test_single_line(self):
|
||||
|
||||
class A(object):
|
||||
a = 1
|
||||
|
||||
@@ -580,13 +570,12 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 == 2
|
||||
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_single_line.<locals>.A'>.a
|
||||
|
||||
failure_demo.py:256: AssertionError
|
||||
failure_demo.py:241: AssertionError
|
||||
____________________ TestCustomAssertMsg.test_multiline ____________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
|
||||
|
||||
def test_multiline(self):
|
||||
|
||||
class A(object):
|
||||
a = 1
|
||||
|
||||
@@ -600,13 +589,12 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 == 2
|
||||
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_multiline.<locals>.A'>.a
|
||||
|
||||
failure_demo.py:264: AssertionError
|
||||
failure_demo.py:248: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
|
||||
|
||||
def test_custom_repr(self):
|
||||
|
||||
class JSON(object):
|
||||
a = 1
|
||||
|
||||
@@ -623,7 +611,7 @@ get on the terminal - we are working on that)::
|
||||
E assert 1 == 2
|
||||
E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a
|
||||
|
||||
failure_demo.py:278: AssertionError
|
||||
failure_demo.py:261: AssertionError
|
||||
============================= warnings summary =============================
|
||||
<undetermined location>
|
||||
Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.
|
||||
|
||||
@@ -357,7 +357,7 @@ which will add info only when run with "--v"::
|
||||
|
||||
$ pytest -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
info1: did you know that ...
|
||||
did you?
|
||||
|
||||
@@ -55,18 +55,18 @@ using it::
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def smtp():
|
||||
def smtp_connection():
|
||||
import smtplib
|
||||
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
|
||||
|
||||
def test_ehlo(smtp):
|
||||
response, msg = smtp.ehlo()
|
||||
def test_ehlo(smtp_connection):
|
||||
response, msg = smtp_connection.ehlo()
|
||||
assert response == 250
|
||||
assert 0 # for demo purposes
|
||||
|
||||
Here, the ``test_ehlo`` needs the ``smtp`` fixture value. pytest
|
||||
Here, the ``test_ehlo`` needs the ``smtp_connection`` fixture value. pytest
|
||||
will discover and call the :py:func:`@pytest.fixture <_pytest.python.fixture>`
|
||||
marked ``smtp`` fixture function. Running the test looks like this::
|
||||
marked ``smtp_connection`` fixture function. Running the test looks like this::
|
||||
|
||||
$ pytest test_smtpsimple.py
|
||||
=========================== test session starts ============================
|
||||
@@ -79,10 +79,10 @@ marked ``smtp`` fixture function. Running the test looks like this::
|
||||
================================= FAILURES =================================
|
||||
________________________________ test_ehlo _________________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
def test_ehlo(smtp):
|
||||
response, msg = smtp.ehlo()
|
||||
def test_ehlo(smtp_connection):
|
||||
response, msg = smtp_connection.ehlo()
|
||||
assert response == 250
|
||||
> assert 0 # for demo purposes
|
||||
E assert 0
|
||||
@@ -91,18 +91,18 @@ marked ``smtp`` fixture function. Running the test looks like this::
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
In the failure traceback we see that the test function was called with a
|
||||
``smtp`` argument, the ``smtplib.SMTP()`` instance created by the fixture
|
||||
``smtp_connection`` argument, the ``smtplib.SMTP()`` instance created by the fixture
|
||||
function. The test function fails on our deliberate ``assert 0``. Here is
|
||||
the exact protocol used by ``pytest`` to call the test function this way:
|
||||
|
||||
1. pytest :ref:`finds <test discovery>` the ``test_ehlo`` because
|
||||
of the ``test_`` prefix. The test function needs a function argument
|
||||
named ``smtp``. A matching fixture function is discovered by
|
||||
looking for a fixture-marked function named ``smtp``.
|
||||
named ``smtp_connection``. A matching fixture function is discovered by
|
||||
looking for a fixture-marked function named ``smtp_connection``.
|
||||
|
||||
2. ``smtp()`` is called to create an instance.
|
||||
2. ``smtp_connection()`` is called to create an instance.
|
||||
|
||||
3. ``test_ehlo(<SMTP instance>)`` is called and fails in the last
|
||||
3. ``test_ehlo(<smtp_connection instance>)`` is called and fails in the last
|
||||
line of the test function.
|
||||
|
||||
Note that if you misspell a function argument or want
|
||||
@@ -154,7 +154,7 @@ This makes use of the automatic caching mechanisms of pytest.
|
||||
Another good approach is by adding the data files in the ``tests`` folder.
|
||||
There are also community plugins available to help managing this aspect of
|
||||
testing, e.g. `pytest-datadir <https://github.com/gabrielcnr/pytest-datadir>`__
|
||||
and `pytest-datafiles <https://pypi.python.org/pypi/pytest-datafiles>`__.
|
||||
and `pytest-datafiles <https://pypi.org/project/pytest-datafiles/>`__.
|
||||
|
||||
.. _smtpshared:
|
||||
|
||||
@@ -167,10 +167,10 @@ Fixtures requiring network access depend on connectivity and are
|
||||
usually time-expensive to create. Extending the previous example, we
|
||||
can add a ``scope="module"`` parameter to the
|
||||
:py:func:`@pytest.fixture <_pytest.python.fixture>` invocation
|
||||
to cause the decorated ``smtp`` fixture function to only be invoked once
|
||||
per test *module* (the default is to invoke once per test *function*).
|
||||
to cause the decorated ``smtp_connection`` fixture function to only be invoked
|
||||
once per test *module* (the default is to invoke once per test *function*).
|
||||
Multiple test functions in a test module will thus
|
||||
each receive the same ``smtp`` fixture instance, thus saving time.
|
||||
each receive the same ``smtp_connection`` fixture instance, thus saving time.
|
||||
|
||||
The next example puts the fixture function into a separate ``conftest.py`` file
|
||||
so that tests from multiple test modules in the directory can
|
||||
@@ -181,23 +181,24 @@ access the fixture function::
|
||||
import smtplib
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def smtp():
|
||||
def smtp_connection():
|
||||
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
|
||||
|
||||
The name of the fixture again is ``smtp`` and you can access its result by
|
||||
listing the name ``smtp`` as an input parameter in any test or fixture
|
||||
function (in or below the directory where ``conftest.py`` is located)::
|
||||
The name of the fixture again is ``smtp_connection`` and you can access its
|
||||
result by listing the name ``smtp_connection`` as an input parameter in any
|
||||
test or fixture function (in or below the directory where ``conftest.py`` is
|
||||
located)::
|
||||
|
||||
# content of test_module.py
|
||||
|
||||
def test_ehlo(smtp):
|
||||
response, msg = smtp.ehlo()
|
||||
def test_ehlo(smtp_connection):
|
||||
response, msg = smtp_connection.ehlo()
|
||||
assert response == 250
|
||||
assert b"smtp.gmail.com" in msg
|
||||
assert 0 # for demo purposes
|
||||
|
||||
def test_noop(smtp):
|
||||
response, msg = smtp.noop()
|
||||
def test_noop(smtp_connection):
|
||||
response, msg = smtp_connection.noop()
|
||||
assert response == 250
|
||||
assert 0 # for demo purposes
|
||||
|
||||
@@ -215,10 +216,10 @@ inspect what is going on and can now run the tests::
|
||||
================================= FAILURES =================================
|
||||
________________________________ test_ehlo _________________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
def test_ehlo(smtp):
|
||||
response, msg = smtp.ehlo()
|
||||
def test_ehlo(smtp_connection):
|
||||
response, msg = smtp_connection.ehlo()
|
||||
assert response == 250
|
||||
assert b"smtp.gmail.com" in msg
|
||||
> assert 0 # for demo purposes
|
||||
@@ -227,10 +228,10 @@ inspect what is going on and can now run the tests::
|
||||
test_module.py:6: AssertionError
|
||||
________________________________ test_noop _________________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
def test_noop(smtp):
|
||||
response, msg = smtp.noop()
|
||||
def test_noop(smtp_connection):
|
||||
response, msg = smtp_connection.noop()
|
||||
assert response == 250
|
||||
> assert 0 # for demo purposes
|
||||
E assert 0
|
||||
@@ -239,18 +240,18 @@ inspect what is going on and can now run the tests::
|
||||
========================= 2 failed in 0.12 seconds =========================
|
||||
|
||||
You see the two ``assert 0`` failing and more importantly you can also see
|
||||
that the same (module-scoped) ``smtp`` object was passed into the two
|
||||
test functions because pytest shows the incoming argument values in the
|
||||
traceback. As a result, the two test functions using ``smtp`` run as
|
||||
quick as a single one because they reuse the same instance.
|
||||
that the same (module-scoped) ``smtp_connection`` object was passed into the
|
||||
two test functions because pytest shows the incoming argument values in the
|
||||
traceback. As a result, the two test functions using ``smtp_connection`` run
|
||||
as quick as a single one because they reuse the same instance.
|
||||
|
||||
If you decide that you rather want to have a session-scoped ``smtp``
|
||||
If you decide that you rather want to have a session-scoped ``smtp_connection``
|
||||
instance, you can simply declare it:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def smtp():
|
||||
def smtp_connection():
|
||||
# the returned fixture value will be shared for
|
||||
# all tests needing it
|
||||
...
|
||||
@@ -258,6 +259,22 @@ instance, you can simply declare it:
|
||||
Finally, the ``class`` scope will invoke the fixture once per test *class*.
|
||||
|
||||
|
||||
``package`` scope (experimental)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. versionadded:: 3.7
|
||||
|
||||
In pytest 3.7 the ``package`` scope has been introduced. Package-scoped fixtures
|
||||
are finalized when the last test of a *package* finishes.
|
||||
|
||||
.. warning::
|
||||
This functionality is considered **experimental** and may be removed in future
|
||||
versions if hidden corner-cases or serious problems with this functionality
|
||||
are discovered after it gets more usage in the wild.
|
||||
|
||||
Use this new feature sparingly and please make sure to report any issues you find.
|
||||
|
||||
|
||||
Higher-scoped fixtures are instantiated first
|
||||
---------------------------------------------
|
||||
|
||||
@@ -323,11 +340,11 @@ the code after the *yield* statement serves as the teardown code:
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def smtp():
|
||||
smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
|
||||
yield smtp # provide the fixture value
|
||||
def smtp_connection():
|
||||
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
|
||||
yield smtp_connection # provide the fixture value
|
||||
print("teardown smtp")
|
||||
smtp.close()
|
||||
smtp_connection.close()
|
||||
|
||||
The ``print`` and ``smtp.close()`` statements will execute when the last test in
|
||||
the module has finished execution, regardless of the exception status of the
|
||||
@@ -340,7 +357,7 @@ Let's execute it::
|
||||
|
||||
2 failed in 0.12 seconds
|
||||
|
||||
We see that the ``smtp`` instance is finalized after the two
|
||||
We see that the ``smtp_connection`` instance is finalized after the two
|
||||
tests finished execution. Note that if we decorated our fixture
|
||||
function with ``scope='function'`` then fixture setup and cleanup would
|
||||
occur around each single test. In either case the test
|
||||
@@ -358,13 +375,13 @@ Note that we can also seamlessly use the ``yield`` syntax with ``with`` statemen
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def smtp():
|
||||
with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp:
|
||||
yield smtp # provide the fixture value
|
||||
def smtp_connection():
|
||||
with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
|
||||
yield smtp_connection # provide the fixture value
|
||||
|
||||
|
||||
The ``smtp`` connection will be closed after the test finished execution
|
||||
because the ``smtp`` object automatically closes when
|
||||
The ``smtp_connection`` connection will be closed after the test finished
|
||||
execution because the ``smtp_connection`` object automatically closes when
|
||||
the ``with`` statement ends.
|
||||
|
||||
Note that if an exception happens during the *setup* code (before the ``yield`` keyword), the
|
||||
@@ -374,7 +391,7 @@ An alternative option for executing *teardown* code is to
|
||||
make use of the ``addfinalizer`` method of the `request-context`_ object to register
|
||||
finalization functions.
|
||||
|
||||
Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup:
|
||||
Here's the ``smtp_connection`` fixture changed to use ``addfinalizer`` for cleanup:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -384,15 +401,15 @@ Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup:
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def smtp(request):
|
||||
smtp = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
|
||||
def smtp_connection(request):
|
||||
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
|
||||
|
||||
def fin():
|
||||
print("teardown smtp")
|
||||
smtp.close()
|
||||
print("teardown smtp_connection")
|
||||
smtp_connection.close()
|
||||
|
||||
request.addfinalizer(fin)
|
||||
return smtp # provide the fixture value
|
||||
return smtp_connection # provide the fixture value
|
||||
|
||||
|
||||
Both ``yield`` and ``addfinalizer`` methods work similarly by calling their code after the test
|
||||
@@ -425,7 +442,7 @@ Fixtures can introspect the requesting test context
|
||||
|
||||
Fixture functions can accept the :py:class:`request <FixtureRequest>` object
|
||||
to introspect the "requesting" test function, class or module context.
|
||||
Further extending the previous ``smtp`` fixture example, let's
|
||||
Further extending the previous ``smtp_connection`` fixture example, let's
|
||||
read an optional server URL from the test module which uses our fixture::
|
||||
|
||||
# content of conftest.py
|
||||
@@ -433,12 +450,12 @@ read an optional server URL from the test module which uses our fixture::
|
||||
import smtplib
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def smtp(request):
|
||||
def smtp_connection(request):
|
||||
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
|
||||
smtp = smtplib.SMTP(server, 587, timeout=5)
|
||||
yield smtp
|
||||
print ("finalizing %s (%s)" % (smtp, server))
|
||||
smtp.close()
|
||||
smtp_connection = smtplib.SMTP(server, 587, timeout=5)
|
||||
yield smtp_connection
|
||||
print ("finalizing %s (%s)" % (smtp_connection, server))
|
||||
smtp_connection.close()
|
||||
|
||||
We use the ``request.module`` attribute to optionally obtain an
|
||||
``smtpserver`` attribute from the test module. If we just execute
|
||||
@@ -456,8 +473,8 @@ server URL in its module namespace::
|
||||
|
||||
smtpserver = "mail.python.org" # will be read by smtp fixture
|
||||
|
||||
def test_showhelo(smtp):
|
||||
assert 0, smtp.helo()
|
||||
def test_showhelo(smtp_connection):
|
||||
assert 0, smtp_connection.helo()
|
||||
|
||||
Running it::
|
||||
|
||||
@@ -466,13 +483,13 @@ Running it::
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_showhelo _______________________________
|
||||
test_anothersmtp.py:5: in test_showhelo
|
||||
assert 0, smtp.helo()
|
||||
assert 0, smtp_connection.helo()
|
||||
E AssertionError: (250, b'mail.python.org')
|
||||
E assert 0
|
||||
------------------------- Captured stdout teardown -------------------------
|
||||
finalizing <smtplib.SMTP object at 0xdeadbeef> (mail.python.org)
|
||||
|
||||
voila! The ``smtp`` fixture function picked up our mail server name
|
||||
voila! The ``smtp_connection`` fixture function picked up our mail server name
|
||||
from the module namespace.
|
||||
|
||||
.. _`fixture-factory`:
|
||||
@@ -535,13 +552,13 @@ Parametrizing fixtures
|
||||
|
||||
Fixture functions can be parametrized in which case they will be called
|
||||
multiple times, each time executing the set of dependent tests, i. e. the
|
||||
tests that depend on this fixture. Test functions do usually not need
|
||||
tests that depend on this fixture. Test functions usually do not need
|
||||
to be aware of their re-running. Fixture parametrization helps to
|
||||
write exhaustive functional tests for components which themselves can be
|
||||
configured in multiple ways.
|
||||
|
||||
Extending the previous example, we can flag the fixture to create two
|
||||
``smtp`` fixture instances which will cause all tests using the fixture
|
||||
``smtp_connection`` fixture instances which will cause all tests using the fixture
|
||||
to run twice. The fixture function gets access to each parameter
|
||||
through the special :py:class:`request <FixtureRequest>` object::
|
||||
|
||||
@@ -551,11 +568,11 @@ through the special :py:class:`request <FixtureRequest>` object::
|
||||
|
||||
@pytest.fixture(scope="module",
|
||||
params=["smtp.gmail.com", "mail.python.org"])
|
||||
def smtp(request):
|
||||
smtp = smtplib.SMTP(request.param, 587, timeout=5)
|
||||
yield smtp
|
||||
print ("finalizing %s" % smtp)
|
||||
smtp.close()
|
||||
def smtp_connection(request):
|
||||
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
|
||||
yield smtp_connection
|
||||
print("finalizing %s" % smtp_connection)
|
||||
smtp_connection.close()
|
||||
|
||||
The main change is the declaration of ``params`` with
|
||||
:py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values
|
||||
@@ -568,10 +585,10 @@ So let's just do another run::
|
||||
================================= FAILURES =================================
|
||||
________________________ test_ehlo[smtp.gmail.com] _________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
def test_ehlo(smtp):
|
||||
response, msg = smtp.ehlo()
|
||||
def test_ehlo(smtp_connection):
|
||||
response, msg = smtp_connection.ehlo()
|
||||
assert response == 250
|
||||
assert b"smtp.gmail.com" in msg
|
||||
> assert 0 # for demo purposes
|
||||
@@ -580,10 +597,10 @@ So let's just do another run::
|
||||
test_module.py:6: AssertionError
|
||||
________________________ test_noop[smtp.gmail.com] _________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
def test_noop(smtp):
|
||||
response, msg = smtp.noop()
|
||||
def test_noop(smtp_connection):
|
||||
response, msg = smtp_connection.noop()
|
||||
assert response == 250
|
||||
> assert 0 # for demo purposes
|
||||
E assert 0
|
||||
@@ -591,10 +608,10 @@ So let's just do another run::
|
||||
test_module.py:11: AssertionError
|
||||
________________________ test_ehlo[mail.python.org] ________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
def test_ehlo(smtp):
|
||||
response, msg = smtp.ehlo()
|
||||
def test_ehlo(smtp_connection):
|
||||
response, msg = smtp_connection.ehlo()
|
||||
assert response == 250
|
||||
> assert b"smtp.gmail.com" in msg
|
||||
E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8'
|
||||
@@ -604,10 +621,10 @@ So let's just do another run::
|
||||
finalizing <smtplib.SMTP object at 0xdeadbeef>
|
||||
________________________ test_noop[mail.python.org] ________________________
|
||||
|
||||
smtp = <smtplib.SMTP object at 0xdeadbeef>
|
||||
smtp_connection = <smtplib.SMTP object at 0xdeadbeef>
|
||||
|
||||
def test_noop(smtp):
|
||||
response, msg = smtp.noop()
|
||||
def test_noop(smtp_connection):
|
||||
response, msg = smtp_connection.noop()
|
||||
assert response == 250
|
||||
> assert 0 # for demo purposes
|
||||
E assert 0
|
||||
@@ -618,7 +635,7 @@ So let's just do another run::
|
||||
4 failed in 0.12 seconds
|
||||
|
||||
We see that our two test functions each ran twice, against the different
|
||||
``smtp`` instances. Note also, that with the ``mail.python.org``
|
||||
``smtp_connection`` instances. Note also, that with the ``mail.python.org``
|
||||
connection the second test fails in ``test_ehlo`` because a
|
||||
different server string is expected than what arrived.
|
||||
|
||||
@@ -709,7 +726,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``::
|
||||
|
||||
$ pytest test_fixture_marks.py -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 3 items
|
||||
@@ -730,46 +747,46 @@ can use other fixtures themselves. This contributes to a modular design
|
||||
of your fixtures and allows re-use of framework-specific fixtures across
|
||||
many projects. As a simple example, we can extend the previous example
|
||||
and instantiate an object ``app`` where we stick the already defined
|
||||
``smtp`` resource into it::
|
||||
``smtp_connection`` resource into it::
|
||||
|
||||
# content of test_appsetup.py
|
||||
|
||||
import pytest
|
||||
|
||||
class App(object):
|
||||
def __init__(self, smtp):
|
||||
self.smtp = smtp
|
||||
def __init__(self, smtp_connection):
|
||||
self.smtp_connection = smtp_connection
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def app(smtp):
|
||||
return App(smtp)
|
||||
def app(smtp_connection):
|
||||
return App(smtp_connection)
|
||||
|
||||
def test_smtp_exists(app):
|
||||
assert app.smtp
|
||||
def test_smtp_connection_exists(app):
|
||||
assert app.smtp_connection
|
||||
|
||||
Here we declare an ``app`` fixture which receives the previously defined
|
||||
``smtp`` fixture and instantiates an ``App`` object with it. Let's run it::
|
||||
``smtp_connection`` fixture and instantiates an ``App`` object with it. Let's run it::
|
||||
|
||||
$ pytest -v test_appsetup.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED [ 50%]
|
||||
test_appsetup.py::test_smtp_exists[mail.python.org] PASSED [100%]
|
||||
test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%]
|
||||
test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%]
|
||||
|
||||
========================= 2 passed in 0.12 seconds =========================
|
||||
|
||||
Due to the parametrization of ``smtp`` the test will run twice with two
|
||||
Due to the parametrization of ``smtp_connection``, the test will run twice with two
|
||||
different ``App`` instances and respective smtp servers. There is no
|
||||
need for the ``app`` fixture to be aware of the ``smtp`` parametrization
|
||||
as pytest will fully analyse the fixture dependency graph.
|
||||
need for the ``app`` fixture to be aware of the ``smtp_connection``
|
||||
parametrization because pytest will fully analyse the fixture dependency graph.
|
||||
|
||||
Note, that the ``app`` fixture has a scope of ``module`` and uses a
|
||||
module-scoped ``smtp`` fixture. The example would still work if ``smtp``
|
||||
was cached on a ``session`` scope: it is fine for fixtures to use
|
||||
module-scoped ``smtp_connection`` fixture. The example would still work if
|
||||
``smtp_connection`` was cached on a ``session`` scope: it is fine for fixtures to use
|
||||
"broader" scoped fixtures but not the other way round:
|
||||
A session-scoped fixture could not use a module-scoped one in a
|
||||
meaningful way.
|
||||
@@ -788,7 +805,7 @@ first execute with one instance and then finalizers are called
|
||||
before the next fixture instance is created. Among other things,
|
||||
this eases testing of applications which create and use global state.
|
||||
|
||||
The following example uses two parametrized fixture, one of which is
|
||||
The following example uses two parametrized fixtures, one of which is
|
||||
scoped on a per-module basis, and all the functions perform ``print`` calls
|
||||
to show the setup/teardown flow::
|
||||
|
||||
@@ -821,7 +838,7 @@ Let's run the tests in verbose mode and with looking at the print-output::
|
||||
|
||||
$ pytest -v -s test_module.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 8 items
|
||||
@@ -943,7 +960,7 @@ a generic feature of the mark mechanism:
|
||||
Note that the assigned variable *must* be called ``pytestmark``, assigning e.g.
|
||||
``foomark`` will not activate the fixtures.
|
||||
|
||||
Lastly you can put fixtures required by all tests in your project
|
||||
It is also possible to put fixtures required by all tests in your project
|
||||
into an ini-file:
|
||||
|
||||
.. code-block:: ini
|
||||
@@ -953,6 +970,22 @@ into an ini-file:
|
||||
usefixtures = cleandir
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
Note this mark has no effect in **fixture functions**. For example,
|
||||
this **will not work as expected**:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.usefixtures("my_other_fixture")
|
||||
@pytest.fixture
|
||||
def my_fixture_that_sadly_wont_use_my_other_fixture():
|
||||
...
|
||||
|
||||
Currently this will not generate any error or warning, but this is intended
|
||||
to be handled by `#3664 <https://github.com/pytest-dev/pytest/issues/3664>`_.
|
||||
|
||||
|
||||
.. _`autouse`:
|
||||
.. _`autouse fixtures`:
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ Install ``pytest``
|
||||
2. Check that you installed the correct version::
|
||||
|
||||
$ pytest --version
|
||||
This is pytest version 3.x.y, imported from $PYTHON_PREFIX/lib/python3.5/site-packages/pytest.py
|
||||
This is pytest version 3.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ You can then install your package in "editable" mode::
|
||||
pip install -e .
|
||||
|
||||
which lets you change your source code (both tests and application) and rerun tests at will.
|
||||
This is similar to running `python setup.py develop` or `conda develop` in that it installs
|
||||
This is similar to running ``python setup.py develop`` or ``conda develop`` in that it installs
|
||||
your package using a symlink to your development code.
|
||||
|
||||
Once you are done with your work and want to make sure that your actual
|
||||
|
||||
@@ -52,22 +52,20 @@ should add ``--strict`` to ``addopts``:
|
||||
serial
|
||||
|
||||
|
||||
.. `marker-iteration`
|
||||
|
||||
Marker revamp and iteration
|
||||
---------------------------
|
||||
|
||||
.. versionadded:: 3.6
|
||||
|
||||
pytest's marker implementation traditionally worked by simply updating the ``__dict__`` attribute of functions to add markers, in a cumulative manner. As a result of the this, markers would unintendely be passed along class hierarchies in surprising ways plus the API for retriving them was inconsistent, as markers from parameterization would be stored differently than markers applied using the ``@pytest.mark`` decorator and markers added via ``node.add_marker``.
|
||||
pytest's marker implementation traditionally worked by simply updating the ``__dict__`` attribute of functions to cumulatively add markers. As a result, markers would unintentionally be passed along class hierarchies in surprising ways. Further, the API for retrieving them was inconsistent, as markers from parameterization would be stored differently than markers applied using the ``@pytest.mark`` decorator and markers added via ``node.add_marker``.
|
||||
|
||||
This state of things made it technically next to impossible to use data from markers correctly without having a deep understanding of the internals, leading to subtle and hard to understand bugs in more advanced usages.
|
||||
|
||||
Depending on how a marker got declared/changed one would get either a ``MarkerInfo`` which might contain markers from sibling classes,
|
||||
``MarkDecorators`` when marks came from parameterization or from a ``node.add_marker`` call, discarding prior marks. Also ``MarkerInfo`` acts like a single mark, when it in fact represents a merged view on multiple marks with the same name.
|
||||
|
||||
On top of that markers where not accessible the same way for modules, classes, and functions/methods,
|
||||
in fact, markers where only accessible in functions, even if they where declared on classes/modules.
|
||||
On top of that markers were not accessible the same way for modules, classes, and functions/methods.
|
||||
In fact, markers were only accessible in functions, even if they were declared on classes/modules.
|
||||
|
||||
A new API to access markers has been introduced in pytest 3.6 in order to solve the problems with the initial design, providing :func:`_pytest.nodes.Node.iter_markers` method to iterate over markers in a consistent manner and reworking the internals, which solved great deal of problems with the initial design.
|
||||
|
||||
@@ -85,7 +83,7 @@ In general there are two scenarios on how markers should be handled:
|
||||
1. Marks overwrite each other. Order matters but you only want to think of your mark as a single item. E.g.
|
||||
``log_level('info')`` at a module level can be overwritten by ``log_level('debug')`` for a specific test.
|
||||
|
||||
In this case replace use ``Node.get_closest_marker(name)``:
|
||||
In this case, use ``Node.get_closest_marker(name)``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -99,7 +97,7 @@ In general there are two scenarios on how markers should be handled:
|
||||
if marker:
|
||||
level = marker.args[0]
|
||||
|
||||
2. Marks compose additive. E.g. ``skipif(condition)`` marks means you just want to evaluate all of them,
|
||||
2. Marks compose in an additive manner. E.g. ``skipif(condition)`` marks mean you just want to evaluate all of them,
|
||||
order doesn't even matter. You probably want to think of your marks as a set here.
|
||||
|
||||
In this case iterate over each mark and handle their ``*args`` and ``**kwargs`` individually.
|
||||
@@ -129,27 +127,27 @@ Here is a non-exhaustive list of issues fixed by the new implementation:
|
||||
|
||||
* Marks don't pick up nested classes (`#199 <https://github.com/pytest-dev/pytest/issues/199>`_).
|
||||
|
||||
* markers stains on all related classes (`#568 <https://github.com/pytest-dev/pytest/issues/568>`_).
|
||||
* Markers stain on all related classes (`#568 <https://github.com/pytest-dev/pytest/issues/568>`_).
|
||||
|
||||
* combining marks - args and kwargs calculation (`#2897 <https://github.com/pytest-dev/pytest/issues/2897>`_).
|
||||
* Combining marks - args and kwargs calculation (`#2897 <https://github.com/pytest-dev/pytest/issues/2897>`_).
|
||||
|
||||
* ``request.node.get_marker('name')`` returns ``None`` for markers applied in classes (`#902 <https://github.com/pytest-dev/pytest/issues/902>`_).
|
||||
|
||||
* marks applied in parametrize are stored as markdecorator (`#2400 <https://github.com/pytest-dev/pytest/issues/2400>`_).
|
||||
* Marks applied in parametrize are stored as markdecorator (`#2400 <https://github.com/pytest-dev/pytest/issues/2400>`_).
|
||||
|
||||
* fix marker interaction in a backward incompatible way (`#1670 <https://github.com/pytest-dev/pytest/issues/1670>`_).
|
||||
* Fix marker interaction in a backward incompatible way (`#1670 <https://github.com/pytest-dev/pytest/issues/1670>`_).
|
||||
|
||||
* Refactor marks to get rid of the current "marks transfer" mechanism (`#2363 <https://github.com/pytest-dev/pytest/issues/2363>`_).
|
||||
|
||||
* Introduce FunctionDefinition node, use it in generate_tests (`#2522 <https://github.com/pytest-dev/pytest/issues/2522>`_).
|
||||
|
||||
* remove named marker attributes and collect markers in items (`#891 <https://github.com/pytest-dev/pytest/issues/891>`_).
|
||||
* Remove named marker attributes and collect markers in items (`#891 <https://github.com/pytest-dev/pytest/issues/891>`_).
|
||||
|
||||
* skipif mark from parametrize hides module level skipif mark (`#1540 <https://github.com/pytest-dev/pytest/issues/1540>`_).
|
||||
|
||||
* skipif + parametrize not skipping tests (`#1296 <https://github.com/pytest-dev/pytest/issues/1296>`_).
|
||||
|
||||
* marker transfer incompatible with inheritance (`#535 <https://github.com/pytest-dev/pytest/issues/535>`_).
|
||||
* Marker transfer incompatible with inheritance (`#535 <https://github.com/pytest-dev/pytest/issues/535>`_).
|
||||
|
||||
More details can be found in the `original PR <https://github.com/pytest-dev/pytest/pull/3317>`_.
|
||||
|
||||
|
||||
@@ -161,6 +161,25 @@ Skip a test function if a condition is ``True``.
|
||||
:keyword str reason: Reason why the test function is being skipped.
|
||||
|
||||
|
||||
.. _`pytest.mark.usefixtures ref`:
|
||||
|
||||
pytest.mark.usefixtures
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Tutorial**: :ref:`usefixtures`.
|
||||
|
||||
Mark a test function as using the given fixture names.
|
||||
|
||||
.. warning::
|
||||
|
||||
This mark can be used with *test functions* only, having no affect when applied
|
||||
to a **fixture** function.
|
||||
|
||||
.. py:function:: pytest.mark.usefixtures(*names)
|
||||
|
||||
:param args: the names of the fixture to use, as strings
|
||||
|
||||
|
||||
.. _`pytest.mark.xfail ref`:
|
||||
|
||||
pytest.mark.xfail
|
||||
@@ -768,7 +787,7 @@ TestReport
|
||||
_Result
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pluggy._Result
|
||||
.. autoclass:: pluggy.callers._Result
|
||||
:members:
|
||||
|
||||
Special Variables
|
||||
|
||||
@@ -80,11 +80,11 @@ during import time.
|
||||
|
||||
If you wish to skip something conditionally then you can use ``skipif`` instead.
|
||||
Here is an example of marking a test function to be skipped
|
||||
when run on a Python3.6 interpreter::
|
||||
when run on an interpreter earlier than Python3.6 ::
|
||||
|
||||
import sys
|
||||
@pytest.mark.skipif(sys.version_info < (3,6),
|
||||
reason="requires python3.6")
|
||||
reason="requires python3.6 or higher")
|
||||
def test_function():
|
||||
...
|
||||
|
||||
|
||||
@@ -171,6 +171,18 @@ for example::
|
||||
>>> sys.last_value
|
||||
AssertionError('assert result == "ok"',)
|
||||
|
||||
.. _trace-option:
|
||||
|
||||
Dropping to PDB_ (Python Debugger) at the start of a test
|
||||
----------------------------------------------------------
|
||||
|
||||
|
||||
``pytest`` allows one to drop into the PDB_ prompt immediately at the start of each test via a command line option::
|
||||
|
||||
pytest --trace
|
||||
|
||||
This will invoke the Python debugger at the start of every test.
|
||||
|
||||
.. _breakpoints:
|
||||
|
||||
Setting breakpoints
|
||||
|
||||
@@ -386,11 +386,52 @@ return a result object, with which we can assert the tests' outcomes.
|
||||
result.assert_outcomes(passed=4)
|
||||
|
||||
|
||||
additionally it is possible to copy examples for a example folder before running pytest on it
|
||||
|
||||
.. code:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
pytester_example_dir = .
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
# content of test_example.py
|
||||
|
||||
|
||||
def test_plugin(testdir):
|
||||
testdir.copy_example("test_example.py")
|
||||
testdir.runpytest("-k", "test_example")
|
||||
|
||||
def test_example():
|
||||
pass
|
||||
|
||||
.. code::
|
||||
|
||||
$ pytest
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini
|
||||
collected 2 items
|
||||
|
||||
test_example.py .. [100%]
|
||||
|
||||
============================= warnings summary =============================
|
||||
test_example.py::test_plugin
|
||||
$REGENDOC_TMPDIR/test_example.py:4: PytestExerimentalApiWarning: testdir.copy_example is an experimental api that may change over time
|
||||
testdir.copy_example("test_example.py")
|
||||
|
||||
-- Docs: http://doc.pytest.org/en/latest/warnings.html
|
||||
=================== 2 passed, 1 warnings in 0.12 seconds ===================
|
||||
|
||||
For more information about the result object that ``runpytest()`` returns, and
|
||||
the methods that it provides please check out the :py:class:`RunResult
|
||||
<_pytest.pytester.RunResult>` documentation.
|
||||
|
||||
|
||||
|
||||
|
||||
.. _`writinghooks`:
|
||||
|
||||
Writing hook functions
|
||||
|
||||
@@ -10,6 +10,7 @@ package = "pytest"
|
||||
package_dir = "src"
|
||||
filename = "CHANGELOG.rst"
|
||||
directory = "changelog/"
|
||||
title_format = "pytest {version} ({project_date})"
|
||||
template = "changelog/_template.rst"
|
||||
|
||||
[[tool.towncrier.type]]
|
||||
|
||||
7
scripts/fail
Executable file
7
scripts/fail
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
"""Used by .pre-commit-config.yaml"""
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(" ".join(sys.argv[1:]))
|
||||
sys.exit(1)
|
||||
111
scripts/release.py
Normal file
111
scripts/release.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
Invoke development tasks.
|
||||
"""
|
||||
import argparse
|
||||
from colorama import init, Fore
|
||||
from pathlib import Path
|
||||
from subprocess import check_output, check_call, call
|
||||
|
||||
|
||||
def announce(version):
|
||||
"""Generates a new release announcement entry in the docs."""
|
||||
# Get our list of authors
|
||||
stdout = check_output(["git", "describe", "--abbrev=0", "--tags"])
|
||||
stdout = stdout.decode("utf-8")
|
||||
last_version = stdout.strip()
|
||||
|
||||
stdout = check_output(
|
||||
["git", "log", "{}..HEAD".format(last_version), "--format=%aN"]
|
||||
)
|
||||
stdout = stdout.decode("utf-8")
|
||||
|
||||
contributors = set(stdout.splitlines())
|
||||
|
||||
template_name = (
|
||||
"release.minor.rst" if version.endswith(".0") else "release.patch.rst"
|
||||
)
|
||||
template_text = (
|
||||
Path(__file__).parent.joinpath(template_name).read_text(encoding="UTF-8")
|
||||
)
|
||||
|
||||
contributors_text = (
|
||||
"\n".join("* {}".format(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.write_text(text, encoding="UTF-8")
|
||||
print(f"{Fore.CYAN}[generate.announce] {Fore.RESET}Generated {target.name}")
|
||||
|
||||
# Update index with the new release entry
|
||||
index_path = Path(__file__).parent.joinpath("../doc/en/announce/index.rst")
|
||||
lines = index_path.read_text(encoding="UTF-8").splitlines()
|
||||
indent = " "
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith("{}release-".format(indent)):
|
||||
new_line = indent + target.stem
|
||||
if line != new_line:
|
||||
lines.insert(index, new_line)
|
||||
index_path.write_text("\n".join(lines) + "\n", encoding="UTF-8")
|
||||
print(
|
||||
f"{Fore.CYAN}[generate.announce] {Fore.RESET}Updated {index_path.name}"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"{Fore.CYAN}[generate.announce] {Fore.RESET}Skip {index_path.name} (already contains release)"
|
||||
)
|
||||
break
|
||||
|
||||
check_call(["git", "add", str(target)])
|
||||
|
||||
|
||||
def regen():
|
||||
"""Call regendoc tool to update examples and pytest output in the docs."""
|
||||
print(f"{Fore.CYAN}[generate.regen] {Fore.RESET}Updating docs")
|
||||
check_call(["tox", "-e", "regen"])
|
||||
|
||||
|
||||
def fix_formatting():
|
||||
"""Runs pre-commit in all files to ensure they are formatted correctly"""
|
||||
print(
|
||||
f"{Fore.CYAN}[generate.fix linting] {Fore.RESET}Fixing formatting using pre-commit"
|
||||
)
|
||||
call(["pre-commit", "run", "--all-files"])
|
||||
|
||||
|
||||
def pre_release(version):
|
||||
"""Generates new docs, release announcements and creates a local tag."""
|
||||
announce(version)
|
||||
regen()
|
||||
changelog(version, write_out=True)
|
||||
fix_formatting()
|
||||
|
||||
msg = "Preparing release version {}".format(version)
|
||||
check_call(["git", "commit", "-a", "-m", msg])
|
||||
|
||||
print()
|
||||
print(f"{Fore.CYAN}[generate.pre_release] {Fore.GREEN}All done!")
|
||||
print()
|
||||
print(f"Please push your branch and open a PR.")
|
||||
|
||||
|
||||
def changelog(version, write_out=False):
|
||||
if write_out:
|
||||
addopts = []
|
||||
else:
|
||||
addopts = ["--draft"]
|
||||
check_call(["towncrier", "--yes", "--version", version] + addopts)
|
||||
|
||||
|
||||
def main():
|
||||
init(autoreset=True)
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("version", help="Release version")
|
||||
options = parser.parse_args()
|
||||
pre_release(options.version)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
6
setup.py
6
setup.py
@@ -69,19 +69,23 @@ def main():
|
||||
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
|
||||
# used by tox.ini to test with pluggy master
|
||||
if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ:
|
||||
install_requires.append("pluggy>=0.5,<0.7")
|
||||
install_requires.append("pluggy>=0.7")
|
||||
environment_marker_support_level = get_environment_marker_support_level()
|
||||
if environment_marker_support_level >= 2:
|
||||
install_requires.append('funcsigs;python_version<"3.0"')
|
||||
install_requires.append('pathlib2>=2.2.0;python_version<"3.6"')
|
||||
install_requires.append('colorama;sys_platform=="win32"')
|
||||
elif environment_marker_support_level == 1:
|
||||
extras_require[':python_version<"3.0"'] = ["funcsigs"]
|
||||
extras_require[':python_version<"3.6"'] = ["pathlib2>=2.2.0"]
|
||||
extras_require[':sys_platform=="win32"'] = ["colorama"]
|
||||
else:
|
||||
if sys.platform == "win32":
|
||||
install_requires.append("colorama")
|
||||
if sys.version_info < (3, 0):
|
||||
install_requires.append("funcsigs")
|
||||
if sys.version_info < (3, 6):
|
||||
install_requires.append("pathlib2>=2.2.0")
|
||||
|
||||
setup(
|
||||
name="pytest",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import inspect
|
||||
import pprint
|
||||
import sys
|
||||
import traceback
|
||||
from inspect import CO_VARARGS, CO_VARKEYWORDS
|
||||
@@ -274,6 +275,7 @@ class Traceback(list):
|
||||
""" Traceback objects encapsulate and offer higher level
|
||||
access to Traceback entries.
|
||||
"""
|
||||
|
||||
Entry = TracebackEntry
|
||||
|
||||
def __init__(self, tb, excinfo=None):
|
||||
@@ -382,8 +384,11 @@ class ExceptionInfo(object):
|
||||
""" wraps sys.exc_info() objects and offers
|
||||
help for navigating the traceback.
|
||||
"""
|
||||
|
||||
_striptext = ""
|
||||
_assert_start_repr = "AssertionError(u'assert " if _PY2 else "AssertionError('assert "
|
||||
_assert_start_repr = (
|
||||
"AssertionError(u'assert " if _PY2 else "AssertionError('assert "
|
||||
)
|
||||
|
||||
def __init__(self, tup=None, exprinfo=None):
|
||||
import _pytest._code
|
||||
@@ -424,7 +429,7 @@ class ExceptionInfo(object):
|
||||
text = text.rstrip()
|
||||
if tryshort:
|
||||
if text.startswith(self._striptext):
|
||||
text = text[len(self._striptext):]
|
||||
text = text[len(self._striptext) :]
|
||||
return text
|
||||
|
||||
def errisinstance(self, exc):
|
||||
@@ -444,6 +449,7 @@ class ExceptionInfo(object):
|
||||
abspath=False,
|
||||
tbfilter=True,
|
||||
funcargs=False,
|
||||
truncate_locals=True,
|
||||
):
|
||||
""" return str()able representation of this exception info.
|
||||
showlocals: show locals per traceback entry
|
||||
@@ -468,6 +474,7 @@ class ExceptionInfo(object):
|
||||
abspath=abspath,
|
||||
tbfilter=tbfilter,
|
||||
funcargs=funcargs,
|
||||
truncate_locals=truncate_locals,
|
||||
)
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
@@ -497,6 +504,7 @@ class ExceptionInfo(object):
|
||||
@attr.s
|
||||
class FormattedExcinfo(object):
|
||||
""" presenting information about failing Functions and Generators. """
|
||||
|
||||
# for traceback entries
|
||||
flow_marker = ">"
|
||||
fail_marker = "E"
|
||||
@@ -506,6 +514,7 @@ class FormattedExcinfo(object):
|
||||
abspath = attr.ib(default=True)
|
||||
tbfilter = attr.ib(default=True)
|
||||
funcargs = attr.ib(default=False)
|
||||
truncate_locals = attr.ib(default=True)
|
||||
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
|
||||
|
||||
def _getindent(self, source):
|
||||
@@ -556,7 +565,7 @@ class FormattedExcinfo(object):
|
||||
for line in source.lines[:line_index]:
|
||||
lines.append(space_prefix + line)
|
||||
lines.append(self.flow_marker + " " + source.lines[line_index])
|
||||
for line in source.lines[line_index + 1:]:
|
||||
for line in source.lines[line_index + 1 :]:
|
||||
lines.append(space_prefix + line)
|
||||
if excinfo is not None:
|
||||
indent = 4 if short else self._getindent(source)
|
||||
@@ -588,7 +597,10 @@ class FormattedExcinfo(object):
|
||||
# This formatting could all be handled by the
|
||||
# _repr() function, which is only reprlib.Repr in
|
||||
# disguise, so is very configurable.
|
||||
str_repr = self._saferepr(value)
|
||||
if self.truncate_locals:
|
||||
str_repr = self._saferepr(value)
|
||||
else:
|
||||
str_repr = pprint.pformat(value)
|
||||
# if len(str_repr) < 70 or not isinstance(value,
|
||||
# (list, tuple, dict)):
|
||||
lines.append("%-10s = %s" % (name, str_repr))
|
||||
@@ -691,7 +703,7 @@ class FormattedExcinfo(object):
|
||||
else:
|
||||
if recursionindex is not None:
|
||||
extraline = "!!! Recursion detected (same locals & position)"
|
||||
traceback = traceback[:recursionindex + 1]
|
||||
traceback = traceback[: recursionindex + 1]
|
||||
else:
|
||||
extraline = None
|
||||
|
||||
@@ -707,7 +719,9 @@ class FormattedExcinfo(object):
|
||||
repr_chain = []
|
||||
e = excinfo.value
|
||||
descr = None
|
||||
while e is not None:
|
||||
seen = set()
|
||||
while e is not None and id(e) not in seen:
|
||||
seen.add(id(e))
|
||||
if excinfo:
|
||||
reprtraceback = self.repr_traceback(excinfo)
|
||||
reprcrash = excinfo._getreprcrash()
|
||||
@@ -722,15 +736,19 @@ class FormattedExcinfo(object):
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
if e.__cause__ is not None:
|
||||
e = e.__cause__
|
||||
excinfo = ExceptionInfo(
|
||||
(type(e), e, e.__traceback__)
|
||||
) if e.__traceback__ else None
|
||||
excinfo = (
|
||||
ExceptionInfo((type(e), e, e.__traceback__))
|
||||
if e.__traceback__
|
||||
else None
|
||||
)
|
||||
descr = "The above exception was the direct cause of the following exception:"
|
||||
elif (e.__context__ is not None and not e.__suppress_context__):
|
||||
elif e.__context__ is not None and not e.__suppress_context__:
|
||||
e = e.__context__
|
||||
excinfo = ExceptionInfo(
|
||||
(type(e), e, e.__traceback__)
|
||||
) if e.__traceback__ else None
|
||||
excinfo = (
|
||||
ExceptionInfo((type(e), e, e.__traceback__))
|
||||
if e.__traceback__
|
||||
else None
|
||||
)
|
||||
descr = "During handling of the above exception, another exception occurred:"
|
||||
else:
|
||||
e = None
|
||||
@@ -739,7 +757,6 @@ class FormattedExcinfo(object):
|
||||
|
||||
|
||||
class TerminalRepr(object):
|
||||
|
||||
def __str__(self):
|
||||
s = self.__unicode__()
|
||||
if _PY2:
|
||||
@@ -759,7 +776,6 @@ class TerminalRepr(object):
|
||||
|
||||
|
||||
class ExceptionRepr(TerminalRepr):
|
||||
|
||||
def __init__(self):
|
||||
self.sections = []
|
||||
|
||||
@@ -773,7 +789,6 @@ class ExceptionRepr(TerminalRepr):
|
||||
|
||||
|
||||
class ExceptionChainRepr(ExceptionRepr):
|
||||
|
||||
def __init__(self, chain):
|
||||
super(ExceptionChainRepr, self).__init__()
|
||||
self.chain = chain
|
||||
@@ -792,7 +807,6 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||
|
||||
|
||||
class ReprExceptionInfo(ExceptionRepr):
|
||||
|
||||
def __init__(self, reprtraceback, reprcrash):
|
||||
super(ReprExceptionInfo, self).__init__()
|
||||
self.reprtraceback = reprtraceback
|
||||
@@ -831,7 +845,6 @@ class ReprTraceback(TerminalRepr):
|
||||
|
||||
|
||||
class ReprTracebackNative(ReprTraceback):
|
||||
|
||||
def __init__(self, tblines):
|
||||
self.style = "native"
|
||||
self.reprentries = [ReprEntryNative(tblines)]
|
||||
@@ -885,7 +898,6 @@ class ReprEntry(TerminalRepr):
|
||||
|
||||
|
||||
class ReprFileLocation(TerminalRepr):
|
||||
|
||||
def __init__(self, path, lineno, message):
|
||||
self.path = str(path)
|
||||
self.lineno = lineno
|
||||
@@ -903,7 +915,6 @@ class ReprFileLocation(TerminalRepr):
|
||||
|
||||
|
||||
class ReprLocals(TerminalRepr):
|
||||
|
||||
def __init__(self, lines):
|
||||
self.lines = lines
|
||||
|
||||
@@ -913,7 +924,6 @@ class ReprLocals(TerminalRepr):
|
||||
|
||||
|
||||
class ReprFuncArgs(TerminalRepr):
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from __future__ import absolute_import, division, generators, print_function
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import ast
|
||||
from ast import PyCF_ONLY_AST as _AST_FLAG
|
||||
@@ -17,6 +17,7 @@ class Source(object):
|
||||
""" an immutable object holding a source code fragment,
|
||||
possibly deindenting it.
|
||||
"""
|
||||
|
||||
_compilecounter = 0
|
||||
|
||||
def __init__(self, *parts, **kwargs):
|
||||
@@ -60,7 +61,7 @@ class Source(object):
|
||||
if key.step not in (None, 1):
|
||||
raise IndexError("cannot slice a Source with a step")
|
||||
newsource = Source()
|
||||
newsource.lines = self.lines[key.start:key.stop]
|
||||
newsource.lines = self.lines[key.start : key.stop]
|
||||
return newsource
|
||||
|
||||
def __len__(self):
|
||||
@@ -151,12 +152,7 @@ class Source(object):
|
||||
return "\n".join(self.lines)
|
||||
|
||||
def compile(
|
||||
self,
|
||||
filename=None,
|
||||
mode="exec",
|
||||
flag=generators.compiler_flag,
|
||||
dont_inherit=0,
|
||||
_genframe=None,
|
||||
self, filename=None, mode="exec", flag=0, dont_inherit=0, _genframe=None
|
||||
):
|
||||
""" return compiled code object. if filename is None
|
||||
invent an artificial filename which displays
|
||||
@@ -178,7 +174,7 @@ class Source(object):
|
||||
except SyntaxError:
|
||||
ex = sys.exc_info()[1]
|
||||
# re-represent syntax errors from parsing python strings
|
||||
msglines = self.lines[:ex.lineno]
|
||||
msglines = self.lines[: ex.lineno]
|
||||
if ex.offset:
|
||||
msglines.append(" " * ex.offset + "^")
|
||||
msglines.append("(code was compiled probably from here: %s)" % filename)
|
||||
@@ -200,9 +196,7 @@ class Source(object):
|
||||
#
|
||||
|
||||
|
||||
def compile_(
|
||||
source, filename=None, mode="exec", flags=generators.compiler_flag, dont_inherit=0
|
||||
):
|
||||
def compile_(source, filename=None, mode="exec", flags=0, dont_inherit=0):
|
||||
""" compile the given source to a raw code object,
|
||||
and maintain an internal cache which allows later
|
||||
retrieval of the source code for the code object
|
||||
@@ -313,7 +307,7 @@ def deindent(lines, offset=None):
|
||||
except (IndentationError, tokenize.TokenError):
|
||||
pass
|
||||
# Add any lines we didn't see. E.g. if an exception was raised.
|
||||
newlines.extend(lines[len(newlines):])
|
||||
newlines.extend(lines[len(newlines) :])
|
||||
return newlines
|
||||
|
||||
|
||||
|
||||
@@ -45,6 +45,14 @@ else:
|
||||
return ast.Call(a, b, c, None, None)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 4):
|
||||
from importlib.util import spec_from_file_location
|
||||
else:
|
||||
|
||||
def spec_from_file_location(*_, **__):
|
||||
return None
|
||||
|
||||
|
||||
class AssertionRewritingHook(object):
|
||||
"""PEP302 Import hook which rewrites asserts."""
|
||||
|
||||
@@ -213,6 +221,8 @@ class AssertionRewritingHook(object):
|
||||
# Normally, this attribute is 3.2+.
|
||||
mod.__cached__ = pyc
|
||||
mod.__loader__ = self
|
||||
# Normally, this attribute is 3.4+
|
||||
mod.__spec__ = spec_from_file_location(name, co.co_filename, loader=self)
|
||||
py.builtin.exec_(co, mod.__dict__)
|
||||
except: # noqa
|
||||
if name in sys.modules:
|
||||
@@ -309,7 +319,7 @@ def _rewrite_test(config, fn):
|
||||
if (
|
||||
not source.startswith(BOM_UTF8)
|
||||
and cookie_re.match(source[0:end1]) is None
|
||||
and cookie_re.match(source[end1 + 1:end2]) is None
|
||||
and cookie_re.match(source[end1 + 1 : end2]) is None
|
||||
):
|
||||
if hasattr(state, "_indecode"):
|
||||
# encodings imported us again, so don't rewrite.
|
||||
@@ -415,20 +425,18 @@ def _format_assertmsg(obj):
|
||||
# contains a newline it gets escaped, however if an object has a
|
||||
# .__repr__() which contains newlines it does not get escaped.
|
||||
# However in either case we want to preserve the newline.
|
||||
if isinstance(obj, six.text_type) or isinstance(obj, six.binary_type):
|
||||
s = obj
|
||||
is_repr = False
|
||||
else:
|
||||
s = py.io.saferepr(obj)
|
||||
is_repr = True
|
||||
if isinstance(s, six.text_type):
|
||||
t = six.text_type
|
||||
else:
|
||||
t = six.binary_type
|
||||
s = s.replace(t("\n"), t("\n~")).replace(t("%"), t("%%"))
|
||||
if is_repr:
|
||||
s = s.replace(t("\\n"), t("\n~"))
|
||||
return s
|
||||
replaces = [(u"\n", u"\n~"), (u"%", u"%%")]
|
||||
if not isinstance(obj, six.string_types):
|
||||
obj = py.io.saferepr(obj)
|
||||
replaces.append((u"\\n", u"\n~"))
|
||||
|
||||
if isinstance(obj, bytes):
|
||||
replaces = [(r1.encode(), r2.encode()) for r1, r2 in replaces]
|
||||
|
||||
for r1, r2 in replaces:
|
||||
obj = obj.replace(r1, r2)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def _should_repr_global_name(obj):
|
||||
|
||||
@@ -322,7 +322,7 @@ def _compare_eq_dict(left, right, verbose=False):
|
||||
def _notin_text(term, text, verbose=False):
|
||||
index = text.find(term)
|
||||
head = text[:index]
|
||||
tail = text[index + len(term):]
|
||||
tail = text[index + len(term) :]
|
||||
correct_text = head + tail
|
||||
diff = _diff_text(correct_text, text, verbose)
|
||||
newdiff = [u("%s is contained here:") % py.io.saferepr(term, maxsize=42)]
|
||||
|
||||
@@ -5,39 +5,50 @@ the name cache was not chosen to ensure pluggy automatically
|
||||
ignores the external pytest-cache
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import py
|
||||
import six
|
||||
import attr
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
from os.path import sep as _sep, altsep as _altsep
|
||||
import shutil
|
||||
|
||||
from . import paths
|
||||
from .compat import _PY2 as PY2, Path
|
||||
|
||||
README_CONTENT = u"""\
|
||||
# pytest cache directory #
|
||||
|
||||
This directory contains data from the pytest's cache plugin,
|
||||
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
|
||||
|
||||
**Do not** commit this to version control.
|
||||
|
||||
See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information.
|
||||
"""
|
||||
|
||||
|
||||
@attr.s
|
||||
class Cache(object):
|
||||
_cachedir = attr.ib(repr=False)
|
||||
_warn = attr.ib(repr=False)
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self._cachedir = Cache.cache_dir_from_config(config)
|
||||
self.trace = config.trace.root.get("cache")
|
||||
if config.getoption("cacheclear"):
|
||||
self.trace("clearing cachedir")
|
||||
if self._cachedir.check():
|
||||
self._cachedir.remove()
|
||||
self._cachedir.mkdir()
|
||||
@classmethod
|
||||
def for_config(cls, config):
|
||||
cachedir = cls.cache_dir_from_config(config)
|
||||
if config.getoption("cacheclear") and cachedir.exists():
|
||||
shutil.rmtree(str(cachedir))
|
||||
cachedir.mkdir()
|
||||
return cls(cachedir, config.warn)
|
||||
|
||||
@staticmethod
|
||||
def cache_dir_from_config(config):
|
||||
cache_dir = config.getini("cache_dir")
|
||||
cache_dir = os.path.expanduser(cache_dir)
|
||||
cache_dir = os.path.expandvars(cache_dir)
|
||||
if os.path.isabs(cache_dir):
|
||||
return py.path.local(cache_dir)
|
||||
else:
|
||||
return config.rootdir.join(cache_dir)
|
||||
return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir)
|
||||
|
||||
def warn(self, fmt, **args):
|
||||
self._warn(code="I9", message=fmt.format(**args) if args else fmt)
|
||||
|
||||
def makedir(self, name):
|
||||
""" return a directory path object with the given name. If the
|
||||
@@ -49,12 +60,15 @@ class Cache(object):
|
||||
Make sure the name contains your plugin or application
|
||||
identifiers to prevent clashes with other cache users.
|
||||
"""
|
||||
if _sep in name or _altsep is not None and _altsep in name:
|
||||
name = Path(name)
|
||||
if len(name.parts) > 1:
|
||||
raise ValueError("name is not allowed to contain path separators")
|
||||
return self._cachedir.ensure_dir("d", name)
|
||||
res = self._cachedir.joinpath("d", name)
|
||||
res.mkdir(exist_ok=True, parents=True)
|
||||
return py.path.local(res)
|
||||
|
||||
def _getvaluepath(self, key):
|
||||
return self._cachedir.join("v", *key.split("/"))
|
||||
return self._cachedir.joinpath("v", Path(key))
|
||||
|
||||
def get(self, key, default):
|
||||
""" return cached value for the given key. If no value
|
||||
@@ -68,13 +82,11 @@ class Cache(object):
|
||||
|
||||
"""
|
||||
path = self._getvaluepath(key)
|
||||
if path.check():
|
||||
try:
|
||||
with path.open("r") as f:
|
||||
return json.load(f)
|
||||
except ValueError:
|
||||
self.trace("cache-invalid at %s" % (path,))
|
||||
return default
|
||||
try:
|
||||
with path.open("r") as f:
|
||||
return json.load(f)
|
||||
except (ValueError, IOError, OSError):
|
||||
return default
|
||||
|
||||
def set(self, key, value):
|
||||
""" save value for the given key.
|
||||
@@ -87,22 +99,25 @@ class Cache(object):
|
||||
"""
|
||||
path = self._getvaluepath(key)
|
||||
try:
|
||||
path.dirpath().ensure_dir()
|
||||
except (py.error.EEXIST, py.error.EACCES):
|
||||
self.config.warn(
|
||||
code="I9", message="could not create cache path %s" % (path,)
|
||||
)
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
except (IOError, OSError):
|
||||
self.warn("could not create cache path {path}", path=path)
|
||||
return
|
||||
try:
|
||||
f = path.open("w")
|
||||
except py.error.ENOTDIR:
|
||||
self.config.warn(
|
||||
code="I9", message="cache could not write path %s" % (path,)
|
||||
)
|
||||
f = path.open("wb" if PY2 else "w")
|
||||
except (IOError, OSError):
|
||||
self.warn("cache could not write path {path}", path=path)
|
||||
else:
|
||||
with f:
|
||||
self.trace("cache-write %s: %r" % (key, value))
|
||||
json.dump(value, f, indent=2, sort_keys=True)
|
||||
self._ensure_readme()
|
||||
|
||||
def _ensure_readme(self):
|
||||
|
||||
if self._cachedir.is_dir():
|
||||
readme_path = self._cachedir / "README.md"
|
||||
if not readme_path.is_file():
|
||||
readme_path.write_text(README_CONTENT)
|
||||
|
||||
|
||||
class LFPlugin(object):
|
||||
@@ -199,9 +214,7 @@ class NFPlugin(object):
|
||||
|
||||
items[:] = self._get_increasing_order(
|
||||
six.itervalues(new_items)
|
||||
) + self._get_increasing_order(
|
||||
six.itervalues(other_items)
|
||||
)
|
||||
) + self._get_increasing_order(six.itervalues(other_items))
|
||||
self.cached_nodeids = [x.nodeid for x in items if isinstance(x, pytest.Item)]
|
||||
|
||||
def _get_increasing_order(self, items):
|
||||
@@ -276,7 +289,7 @@ def pytest_cmdline_main(config):
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_configure(config):
|
||||
config.cache = Cache(config)
|
||||
config.cache = Cache.for_config(config)
|
||||
config.pluginmanager.register(LFPlugin(config), "lfplugin")
|
||||
config.pluginmanager.register(NFPlugin(config), "nfplugin")
|
||||
|
||||
@@ -299,41 +312,47 @@ def cache(request):
|
||||
|
||||
def pytest_report_header(config):
|
||||
if config.option.verbose:
|
||||
relpath = py.path.local().bestrelpath(config.cache._cachedir)
|
||||
return "cachedir: %s" % relpath
|
||||
cachedir = config.cache._cachedir
|
||||
# TODO: evaluate generating upward relative paths
|
||||
# starting with .., ../.. if sensible
|
||||
|
||||
try:
|
||||
displaypath = cachedir.relative_to(config.rootdir)
|
||||
except ValueError:
|
||||
displaypath = cachedir
|
||||
return "cachedir: {}".format(displaypath)
|
||||
|
||||
|
||||
def cacheshow(config, session):
|
||||
from pprint import pprint
|
||||
from pprint import pformat
|
||||
|
||||
tw = py.io.TerminalWriter()
|
||||
tw.line("cachedir: " + str(config.cache._cachedir))
|
||||
if not config.cache._cachedir.check():
|
||||
if not config.cache._cachedir.is_dir():
|
||||
tw.line("cache is empty")
|
||||
return 0
|
||||
dummy = object()
|
||||
basedir = config.cache._cachedir
|
||||
vdir = basedir.join("v")
|
||||
vdir = basedir / "v"
|
||||
tw.sep("-", "cache values")
|
||||
for valpath in sorted(vdir.visit(lambda x: x.isfile())):
|
||||
key = valpath.relto(vdir).replace(valpath.sep, "/")
|
||||
for valpath in sorted(x for x in vdir.rglob("*") if x.is_file()):
|
||||
key = valpath.relative_to(vdir)
|
||||
val = config.cache.get(key, dummy)
|
||||
if val is dummy:
|
||||
tw.line("%s contains unreadable content, " "will be ignored" % key)
|
||||
else:
|
||||
tw.line("%s contains:" % key)
|
||||
stream = py.io.TextIO()
|
||||
pprint(val, stream=stream)
|
||||
for line in stream.getvalue().splitlines():
|
||||
for line in pformat(val).splitlines():
|
||||
tw.line(" " + line)
|
||||
|
||||
ddir = basedir.join("d")
|
||||
if ddir.isdir() and ddir.listdir():
|
||||
ddir = basedir / "d"
|
||||
if ddir.is_dir():
|
||||
contents = sorted(ddir.rglob("*"))
|
||||
tw.sep("-", "cache directories")
|
||||
for p in sorted(basedir.join("d").visit()):
|
||||
for p in contents:
|
||||
# if p.check(dir=1):
|
||||
# print("%s/" % p.relto(basedir))
|
||||
if p.isfile():
|
||||
key = p.relto(basedir)
|
||||
tw.line("%s is a file of length %d" % (key, p.size()))
|
||||
if p.is_file():
|
||||
key = p.relative_to(basedir)
|
||||
tw.line("{} is a file of length {:d}".format(key, p.stat().st_size))
|
||||
return 0
|
||||
|
||||
@@ -531,7 +531,6 @@ class FDCapture(FDCaptureBinary):
|
||||
|
||||
|
||||
class SysCapture(object):
|
||||
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
name = patchsysdict[fd]
|
||||
self._old = getattr(sys, name)
|
||||
@@ -569,7 +568,6 @@ class SysCapture(object):
|
||||
|
||||
|
||||
class SysCaptureBinary(SysCapture):
|
||||
|
||||
def snap(self):
|
||||
res = self.tmpfile.buffer.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
|
||||
@@ -22,6 +22,7 @@ except ImportError: # pragma: no cover
|
||||
# Only available in Python 3.4+ or as a backport
|
||||
enum = None
|
||||
|
||||
__all__ = ["Path"]
|
||||
|
||||
_PY3 = sys.version_info > (3, 0)
|
||||
_PY2 = not _PY3
|
||||
@@ -32,7 +33,6 @@ if _PY3:
|
||||
else:
|
||||
from funcsigs import signature, Parameter as Parameter
|
||||
|
||||
|
||||
NoneType = type(None)
|
||||
NOTSET = object()
|
||||
|
||||
@@ -40,9 +40,15 @@ PY35 = sys.version_info[:2] >= (3, 5)
|
||||
PY36 = sys.version_info[:2] >= (3, 6)
|
||||
MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError"
|
||||
|
||||
if PY36:
|
||||
from pathlib import Path
|
||||
else:
|
||||
from pathlib2 import Path
|
||||
|
||||
|
||||
if _PY3:
|
||||
from collections.abc import MutableMapping as MappingMixin # noqa
|
||||
from collections.abc import Mapping, Sequence # noqa
|
||||
from collections.abc import MutableMapping as MappingMixin
|
||||
from collections.abc import Mapping, Sequence
|
||||
else:
|
||||
# those raise DeprecationWarnings in Python >=3.7
|
||||
from collections import MutableMapping as MappingMixin # noqa
|
||||
@@ -72,16 +78,13 @@ def iscoroutinefunction(func):
|
||||
Note: copied and modified from Python 3.5's builtin couroutines.py to avoid import asyncio directly,
|
||||
which in turns also initializes the "logging" module as side-effect (see issue #8).
|
||||
"""
|
||||
return (
|
||||
getattr(func, "_is_coroutine", False)
|
||||
or (
|
||||
hasattr(inspect, "iscoroutinefunction")
|
||||
and inspect.iscoroutinefunction(func)
|
||||
)
|
||||
return getattr(func, "_is_coroutine", False) or (
|
||||
hasattr(inspect, "iscoroutinefunction") and inspect.iscoroutinefunction(func)
|
||||
)
|
||||
|
||||
|
||||
def getlocation(function, curdir):
|
||||
function = get_real_func(function)
|
||||
fn = py.path.local(inspect.getfile(function))
|
||||
lineno = function.__code__.co_firstlineno
|
||||
if fn.relto(curdir):
|
||||
@@ -138,17 +141,13 @@ def getfuncargnames(function, is_method=False, cls=None):
|
||||
# If this function should be treated as a bound method even though
|
||||
# it's passed as an unbound method or function, remove the first
|
||||
# parameter name.
|
||||
if (
|
||||
is_method
|
||||
or (
|
||||
cls
|
||||
and not isinstance(cls.__dict__.get(function.__name__, None), staticmethod)
|
||||
)
|
||||
if is_method or (
|
||||
cls and not isinstance(cls.__dict__.get(function.__name__, None), staticmethod)
|
||||
):
|
||||
arg_names = arg_names[1:]
|
||||
# Remove any names that will be replaced with mocks.
|
||||
if hasattr(function, "__wrapped__"):
|
||||
arg_names = arg_names[num_mock_patch_args(function):]
|
||||
arg_names = arg_names[num_mock_patch_args(function) :]
|
||||
return arg_names
|
||||
|
||||
|
||||
@@ -229,12 +228,31 @@ else:
|
||||
return val.encode("unicode-escape")
|
||||
|
||||
|
||||
class _PytestWrapper(object):
|
||||
"""Dummy wrapper around a function object for internal use only.
|
||||
|
||||
Used to correctly unwrap the underlying function object
|
||||
when we are creating fixtures, because we wrap the function object ourselves with a decorator
|
||||
to issue warnings when the fixture function is called directly.
|
||||
"""
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
|
||||
|
||||
def get_real_func(obj):
|
||||
""" gets the real function object of the (possibly) wrapped object by
|
||||
functools.wraps or functools.partial.
|
||||
"""
|
||||
start_obj = obj
|
||||
for i in range(100):
|
||||
# __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
|
||||
# to trigger a warning if it gets called directly instead of by pytest: we don't
|
||||
# want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774)
|
||||
new_obj = getattr(obj, "__pytest_wrapped__", None)
|
||||
if isinstance(new_obj, _PytestWrapper):
|
||||
obj = new_obj.obj
|
||||
break
|
||||
new_obj = getattr(obj, "__wrapped__", None)
|
||||
if new_obj is None:
|
||||
break
|
||||
@@ -250,6 +268,21 @@ def get_real_func(obj):
|
||||
return obj
|
||||
|
||||
|
||||
def get_real_method(obj, holder):
|
||||
"""
|
||||
Attempts to obtain the real function object that might be wrapping ``obj``, while at the same time
|
||||
returning a bound method to ``holder`` if the original object was a bound method.
|
||||
"""
|
||||
try:
|
||||
is_method = hasattr(obj, "__func__")
|
||||
obj = get_real_func(obj)
|
||||
except Exception:
|
||||
return obj
|
||||
if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
|
||||
obj = obj.__get__(holder)
|
||||
return obj
|
||||
|
||||
|
||||
def getfslineno(obj):
|
||||
# xxx let decorators etc specify a sane ordering
|
||||
obj = get_real_func(obj)
|
||||
@@ -340,7 +373,6 @@ if _PY2:
|
||||
from py.io import TextIO
|
||||
|
||||
class CaptureIO(TextIO):
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
return getattr(self, "_encoding", "UTF-8")
|
||||
@@ -350,7 +382,6 @@ else:
|
||||
import io
|
||||
|
||||
class CaptureIO(io.TextIOWrapper):
|
||||
|
||||
def __init__(self):
|
||||
super(CaptureIO, self).__init__(
|
||||
io.BytesIO(), encoding="UTF-8", newline="", write_through=True
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
""" command line options, ini-file and conftest.py processing. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import argparse
|
||||
import inspect
|
||||
import shlex
|
||||
import traceback
|
||||
import types
|
||||
@@ -30,7 +31,6 @@ hookspec = HookspecMarker("pytest")
|
||||
|
||||
|
||||
class ConftestImportFailure(Exception):
|
||||
|
||||
def __init__(self, path, excinfo):
|
||||
Exception.__init__(self, path, excinfo)
|
||||
self.path = path
|
||||
@@ -72,7 +72,7 @@ def main(args=None, plugins=None):
|
||||
return 4
|
||||
|
||||
|
||||
class cmdline(object): # NOQA compatibility namespace
|
||||
class cmdline(object): # compatibility namespace
|
||||
main = staticmethod(main)
|
||||
|
||||
|
||||
@@ -253,6 +253,10 @@ class PytestPluginManager(PluginManager):
|
||||
method = getattr(plugin, name)
|
||||
opts = super(PytestPluginManager, self).parse_hookimpl_opts(plugin, name)
|
||||
|
||||
# consider only actual functions for hooks (#3775)
|
||||
if not inspect.isroutine(method):
|
||||
return
|
||||
|
||||
# collect unmarked hooks as long as they have the `pytest_' prefix
|
||||
if opts is None and name.startswith("pytest_"):
|
||||
opts = {}
|
||||
@@ -318,9 +322,11 @@ class PytestPluginManager(PluginManager):
|
||||
self._configured = True
|
||||
|
||||
def _warn(self, message):
|
||||
kwargs = message if isinstance(message, dict) else {
|
||||
"code": "I1", "message": message, "fslocation": None, "nodeid": None
|
||||
}
|
||||
kwargs = (
|
||||
message
|
||||
if isinstance(message, dict)
|
||||
else {"code": "I1", "message": message, "fslocation": None, "nodeid": None}
|
||||
)
|
||||
self.hook.pytest_logwarning.call_historic(kwargs=kwargs)
|
||||
|
||||
#
|
||||
@@ -335,9 +341,11 @@ class PytestPluginManager(PluginManager):
|
||||
here.
|
||||
"""
|
||||
current = py.path.local()
|
||||
self._confcutdir = current.join(
|
||||
namespace.confcutdir, abs=True
|
||||
) if namespace.confcutdir else None
|
||||
self._confcutdir = (
|
||||
current.join(namespace.confcutdir, abs=True)
|
||||
if namespace.confcutdir
|
||||
else None
|
||||
)
|
||||
self._noconftest = namespace.noconftest
|
||||
testpaths = namespace.file_or_dir
|
||||
foundanchor = False
|
||||
@@ -405,7 +413,9 @@ class PytestPluginManager(PluginManager):
|
||||
try:
|
||||
mod = conftestpath.pyimport()
|
||||
if hasattr(mod, "pytest_plugins") and self._configured:
|
||||
from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
|
||||
from _pytest.deprecated import (
|
||||
PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
|
||||
)
|
||||
|
||||
warnings.warn(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST)
|
||||
except Exception:
|
||||
@@ -477,7 +487,8 @@ class PytestPluginManager(PluginManager):
|
||||
except ImportError as e:
|
||||
new_exc_type = ImportError
|
||||
new_exc_message = 'Error importing plugin "%s": %s' % (
|
||||
modname, safe_str(e.args[0])
|
||||
modname,
|
||||
safe_str(e.args[0]),
|
||||
)
|
||||
new_exc = new_exc_type(new_exc_message)
|
||||
|
||||
@@ -518,7 +529,6 @@ def _ensure_removed_sysmodule(modname):
|
||||
|
||||
|
||||
class Notset(object):
|
||||
|
||||
def __repr__(self):
|
||||
return "<NOTSET>"
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@ class Argument(object):
|
||||
and ignoring choices and integer prefixes
|
||||
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
|
||||
"""
|
||||
|
||||
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
|
||||
|
||||
def __init__(self, *names, **attrs):
|
||||
@@ -173,23 +174,23 @@ class Argument(object):
|
||||
if isinstance(typ, six.string_types):
|
||||
if typ == "choice":
|
||||
warnings.warn(
|
||||
"type argument to addoption() is a string %r."
|
||||
" For parsearg this is optional and when supplied"
|
||||
" should be a type."
|
||||
"`type` argument to addoption() is the string %r."
|
||||
" For choices this is optional and can be omitted, "
|
||||
" but when supplied should be a type (for example `str` or `int`)."
|
||||
" (options: %s)" % (typ, names),
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
stacklevel=4,
|
||||
)
|
||||
# argparse expects a type here take it from
|
||||
# the type of the first element
|
||||
attrs["type"] = type(attrs["choices"][0])
|
||||
else:
|
||||
warnings.warn(
|
||||
"type argument to addoption() is a string %r."
|
||||
" For parsearg this should be a type."
|
||||
"`type` argument to addoption() is the string %r, "
|
||||
" but when supplied should be a type (for example `str` or `int`)."
|
||||
" (options: %s)" % (typ, names),
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
stacklevel=4,
|
||||
)
|
||||
attrs["type"] = Argument._typ_map[typ]
|
||||
# used in test_parseopt -> test_parse_defaultgetter
|
||||
@@ -274,7 +275,6 @@ class Argument(object):
|
||||
|
||||
|
||||
class OptionGroup(object):
|
||||
|
||||
def __init__(self, name, description="", parser=None):
|
||||
self.name = name
|
||||
self.description = description
|
||||
@@ -312,7 +312,6 @@ class OptionGroup(object):
|
||||
|
||||
|
||||
class MyOptionParser(argparse.ArgumentParser):
|
||||
|
||||
def __init__(self, parser, extra_info=None):
|
||||
if not extra_info:
|
||||
extra_info = {}
|
||||
@@ -378,9 +377,8 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
xxoption = option[2:]
|
||||
if xxoption.split()[0] not in option_map:
|
||||
shortened = xxoption.replace("-", "")
|
||||
if (
|
||||
shortened not in short_long
|
||||
or len(short_long[shortened]) < len(xxoption)
|
||||
if shortened not in short_long or len(short_long[shortened]) < len(
|
||||
xxoption
|
||||
):
|
||||
short_long[shortened] = xxoption
|
||||
# now short_long has been filled out to the longest with dashes
|
||||
|
||||
@@ -5,4 +5,5 @@ class UsageError(Exception):
|
||||
class PrintHelp(Exception):
|
||||
"""Raised when pytest should print it's help to skip the rest of the
|
||||
argument parsing and validation."""
|
||||
|
||||
pass
|
||||
|
||||
@@ -74,7 +74,6 @@ def get_common_ancestor(paths):
|
||||
|
||||
|
||||
def get_dirs_from_args(args):
|
||||
|
||||
def is_option(x):
|
||||
return str(x).startswith("-")
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import sys
|
||||
import os
|
||||
from doctest import UnexpectedException
|
||||
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
try:
|
||||
from builtins import breakpoint # noqa
|
||||
|
||||
@@ -28,6 +30,12 @@ def pytest_addoption(parser):
|
||||
help="start a custom interactive Python debugger on errors. "
|
||||
"For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
|
||||
)
|
||||
group._addoption(
|
||||
"--trace",
|
||||
dest="trace",
|
||||
action="store_true",
|
||||
help="Immediately break when running each test.",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
@@ -38,6 +46,8 @@ def pytest_configure(config):
|
||||
else:
|
||||
pdb_cls = pdb.Pdb
|
||||
|
||||
if config.getvalue("trace"):
|
||||
config.pluginmanager.register(PdbTrace(), "pdbtrace")
|
||||
if config.getvalue("usepdb"):
|
||||
config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
|
||||
|
||||
@@ -65,12 +75,13 @@ def pytest_configure(config):
|
||||
|
||||
class pytestPDB(object):
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
|
||||
_pluginmanager = None
|
||||
_config = None
|
||||
_pdb_cls = pdb.Pdb
|
||||
|
||||
@classmethod
|
||||
def set_trace(cls):
|
||||
def set_trace(cls, set_break=True):
|
||||
""" invoke PDB set_trace debugging, dropping any IO capturing. """
|
||||
import _pytest.config
|
||||
|
||||
@@ -83,11 +94,11 @@ class pytestPDB(object):
|
||||
tw.line()
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config)
|
||||
cls._pdb_cls().set_trace(frame)
|
||||
if set_break:
|
||||
cls._pdb_cls().set_trace(frame)
|
||||
|
||||
|
||||
class PdbInvoke(object):
|
||||
|
||||
def pytest_exception_interact(self, node, call, report):
|
||||
capman = node.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
@@ -104,6 +115,30 @@ class PdbInvoke(object):
|
||||
post_mortem(tb)
|
||||
|
||||
|
||||
class PdbTrace(object):
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem):
|
||||
_test_pytest_function(pyfuncitem)
|
||||
yield
|
||||
|
||||
|
||||
def _test_pytest_function(pyfuncitem):
|
||||
pytestPDB.set_trace(set_break=False)
|
||||
testfunction = pyfuncitem.obj
|
||||
pyfuncitem.obj = pdb.runcall
|
||||
if pyfuncitem._isyieldedfunction():
|
||||
arg_list = list(pyfuncitem._args)
|
||||
arg_list.insert(0, testfunction)
|
||||
pyfuncitem._args = tuple(arg_list)
|
||||
else:
|
||||
if "func" in pyfuncitem._fixtureinfo.argnames:
|
||||
raise ValueError("--trace can't be used with a fixture named func!")
|
||||
pyfuncitem.funcargs["func"] = testfunction
|
||||
new_list = list(pyfuncitem._fixtureinfo.argnames)
|
||||
new_list.append("func")
|
||||
pyfuncitem._fixtureinfo.argnames = tuple(new_list)
|
||||
|
||||
|
||||
def _enter_pdb(node, excinfo, rep):
|
||||
# XXX we re-use the TerminalReporter's terminalwriter
|
||||
# because this seems to avoid some encoding related troubles
|
||||
@@ -114,7 +149,9 @@ def _enter_pdb(node, excinfo, rep):
|
||||
showcapture = node.config.option.showcapture
|
||||
|
||||
for sectionname, content in (
|
||||
("stdout", rep.capstdout), ("stderr", rep.capstderr), ("log", rep.caplog)
|
||||
("stdout", rep.capstdout),
|
||||
("stderr", rep.capstderr),
|
||||
("log", rep.caplog),
|
||||
):
|
||||
if showcapture in (sectionname, "all") and content:
|
||||
tw.sep(">", "captured " + sectionname)
|
||||
@@ -148,9 +185,7 @@ def _find_last_non_hidden_frame(stack):
|
||||
|
||||
|
||||
def post_mortem(t):
|
||||
|
||||
class Pdb(pytestPDB._pdb_cls):
|
||||
|
||||
def get_stack(self, f, t):
|
||||
stack, i = pdb.Pdb.get_stack(self, f, t)
|
||||
if f is None:
|
||||
|
||||
@@ -22,7 +22,15 @@ FUNCARG_PREFIX = (
|
||||
"Please remove the prefix and use the @pytest.fixture decorator instead."
|
||||
)
|
||||
|
||||
CFG_PYTEST_SECTION = "[pytest] section in {filename} files is deprecated, use [tool:pytest] instead."
|
||||
FIXTURE_FUNCTION_CALL = (
|
||||
"Fixture {name} called directly. Fixtures are not meant to be called directly, "
|
||||
"are created automatically when test functions request them as parameters. "
|
||||
"See https://docs.pytest.org/en/latest/fixture.html for more information."
|
||||
)
|
||||
|
||||
CFG_PYTEST_SECTION = (
|
||||
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead."
|
||||
)
|
||||
|
||||
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue"
|
||||
|
||||
@@ -63,3 +71,7 @@ PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = RemovedInPytest4Warning(
|
||||
"because it affects the entire directory tree in a non-explicit way.\n"
|
||||
"Please move it to the top level conftest file instead."
|
||||
)
|
||||
|
||||
PYTEST_NAMESPACE = RemovedInPytest4Warning(
|
||||
"pytest_namespace is deprecated and will be removed soon"
|
||||
)
|
||||
|
||||
@@ -105,7 +105,6 @@ def _is_doctest(config, path, parent):
|
||||
|
||||
|
||||
class ReprFailDoctest(TerminalRepr):
|
||||
|
||||
def __init__(self, reprlocation_lines):
|
||||
# List of (reprlocation, lines) tuples
|
||||
self.reprlocation_lines = reprlocation_lines
|
||||
@@ -118,7 +117,6 @@ class ReprFailDoctest(TerminalRepr):
|
||||
|
||||
|
||||
class MultipleDoctestFailures(Exception):
|
||||
|
||||
def __init__(self, failures):
|
||||
super(MultipleDoctestFailures, self).__init__()
|
||||
self.failures = failures
|
||||
@@ -172,7 +170,6 @@ def _get_runner(checker=None, verbose=None, optionflags=0, continue_on_failure=T
|
||||
|
||||
|
||||
class DoctestItem(pytest.Item):
|
||||
|
||||
def __init__(self, name, parent, runner=None, dtest=None):
|
||||
super(DoctestItem, self).__init__(name, parent)
|
||||
self.runner = runner
|
||||
@@ -243,7 +240,7 @@ class DoctestItem(pytest.Item):
|
||||
for (i, x) in enumerate(lines)
|
||||
]
|
||||
# trim docstring error lines to 10
|
||||
lines = lines[max(example.lineno - 9, 0):example.lineno + 1]
|
||||
lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
|
||||
else:
|
||||
lines = [
|
||||
"EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
|
||||
@@ -255,9 +252,7 @@ class DoctestItem(pytest.Item):
|
||||
if isinstance(failure, doctest.DocTestFailure):
|
||||
lines += checker.output_difference(
|
||||
example, failure.got, report_choice
|
||||
).split(
|
||||
"\n"
|
||||
)
|
||||
).split("\n")
|
||||
else:
|
||||
inner_excinfo = ExceptionInfo(failure.exc_info)
|
||||
lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
|
||||
@@ -347,7 +342,6 @@ def _check_all_skipped(test):
|
||||
|
||||
|
||||
class DoctestModule(pytest.Module):
|
||||
|
||||
def collect(self):
|
||||
import doctest
|
||||
|
||||
@@ -480,9 +474,7 @@ def _get_report_choice(key):
|
||||
DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
|
||||
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
|
||||
DOCTEST_REPORT_CHOICE_NONE: 0,
|
||||
}[
|
||||
key
|
||||
]
|
||||
}[key]
|
||||
|
||||
|
||||
def _fix_spoof_python2(runner, encoding):
|
||||
@@ -502,7 +494,6 @@ def _fix_spoof_python2(runner, encoding):
|
||||
from doctest import _SpoofOut
|
||||
|
||||
class UnicodeSpoof(_SpoofOut):
|
||||
|
||||
def getvalue(self):
|
||||
result = _SpoofOut.getvalue(self)
|
||||
if encoding and isinstance(result, bytes):
|
||||
|
||||
13
src/_pytest/experiments.py
Normal file
13
src/_pytest/experiments.py
Normal file
@@ -0,0 +1,13 @@
|
||||
class PytestExerimentalApiWarning(FutureWarning):
|
||||
"warning category used to denote experiments in pytest"
|
||||
|
||||
@classmethod
|
||||
def simple(cls, apiname):
|
||||
return cls(
|
||||
"{apiname} is an experimental api that may change over time".format(
|
||||
apiname=apiname
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
PYTESTER_COPY_EXAMPLE = PytestExerimentalApiWarning.simple("testdir.copy_example")
|
||||
@@ -2,9 +2,12 @@ from __future__ import absolute_import, division, print_function
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from collections import OrderedDict, deque, defaultdict
|
||||
|
||||
import six
|
||||
from more_itertools import flatten
|
||||
|
||||
import attr
|
||||
@@ -27,7 +30,10 @@ from _pytest.compat import (
|
||||
getfuncargnames,
|
||||
safe_getattr,
|
||||
FuncargnamesCompatAttr,
|
||||
get_real_method,
|
||||
_PytestWrapper,
|
||||
)
|
||||
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
|
||||
from _pytest.outcomes import fail, TEST_OUTCOME
|
||||
|
||||
FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}'
|
||||
@@ -45,6 +51,7 @@ def pytest_sessionstart(session):
|
||||
|
||||
scopename2class.update(
|
||||
{
|
||||
"package": _pytest.python.Package,
|
||||
"class": _pytest.python.Class,
|
||||
"module": _pytest.python.Module,
|
||||
"function": _pytest.nodes.Item,
|
||||
@@ -58,6 +65,7 @@ scopename2class = {}
|
||||
|
||||
|
||||
scope2props = dict(session=())
|
||||
scope2props["package"] = ("fspath",)
|
||||
scope2props["module"] = ("fspath", "module")
|
||||
scope2props["class"] = scope2props["module"] + ("cls",)
|
||||
scope2props["instance"] = scope2props["class"] + ("instance",)
|
||||
@@ -65,7 +73,6 @@ scope2props["function"] = scope2props["instance"] + ("function", "keywords")
|
||||
|
||||
|
||||
def scopeproperty(name=None, doc=None):
|
||||
|
||||
def decoratescope(func):
|
||||
scopename = name or func.__name__
|
||||
|
||||
@@ -81,6 +88,21 @@ def scopeproperty(name=None, doc=None):
|
||||
return decoratescope
|
||||
|
||||
|
||||
def get_scope_package(node, fixturedef):
|
||||
import pytest
|
||||
|
||||
cls = pytest.Package
|
||||
current = node
|
||||
fixture_package_name = os.path.join(fixturedef.baseid, "__init__.py")
|
||||
while current and (
|
||||
type(current) is not cls or fixture_package_name != current.nodeid
|
||||
):
|
||||
current = current.parent
|
||||
if current is None:
|
||||
return node.session
|
||||
return current
|
||||
|
||||
|
||||
def get_scope_node(node, scope):
|
||||
cls = scopename2class.get(scope)
|
||||
if cls is None:
|
||||
@@ -174,9 +196,11 @@ def get_parametrized_fixture_keys(item, scopenum):
|
||||
continue
|
||||
if scopenum == 0: # session
|
||||
key = (argname, param_index)
|
||||
elif scopenum == 1: # module
|
||||
elif scopenum == 1: # package
|
||||
key = (argname, param_index, item.fspath.dirpath())
|
||||
elif scopenum == 2: # module
|
||||
key = (argname, param_index, item.fspath)
|
||||
elif scopenum == 2: # class
|
||||
elif scopenum == 3: # class
|
||||
key = (argname, param_index, item.fspath, item.cls)
|
||||
yield key
|
||||
|
||||
@@ -275,12 +299,43 @@ def get_direct_param_fixture_func(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class FuncFixtureInfo(object):
|
||||
# original function argument names
|
||||
argnames = attr.ib(type=tuple)
|
||||
# argnames that function immediately requires. These include argnames +
|
||||
# fixture names specified via usefixtures and via autouse=True in fixture
|
||||
# definitions.
|
||||
initialnames = attr.ib(type=tuple)
|
||||
names_closure = attr.ib(type="List[str]")
|
||||
name2fixturedefs = attr.ib(type="List[str, List[FixtureDef]]")
|
||||
|
||||
def __init__(self, argnames, names_closure, name2fixturedefs):
|
||||
self.argnames = argnames
|
||||
self.names_closure = names_closure
|
||||
self.name2fixturedefs = name2fixturedefs
|
||||
def prune_dependency_tree(self):
|
||||
"""Recompute names_closure from initialnames and name2fixturedefs
|
||||
|
||||
Can only reduce names_closure, which means that the new closure will
|
||||
always be a subset of the old one. The order is preserved.
|
||||
|
||||
This method is needed because direct parametrization may shadow some
|
||||
of the fixtures that were included in the originally built dependency
|
||||
tree. In this way the dependency tree can get pruned, and the closure
|
||||
of argnames may get reduced.
|
||||
"""
|
||||
closure = set()
|
||||
working_set = set(self.initialnames)
|
||||
while working_set:
|
||||
argname = working_set.pop()
|
||||
# argname may be smth not included in the original names_closure,
|
||||
# in which case we ignore it. This currently happens with pseudo
|
||||
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
|
||||
# So they introduce the new dependency 'request' which might have
|
||||
# been missing in the original tree (closure).
|
||||
if argname not in closure and argname in self.names_closure:
|
||||
closure.add(argname)
|
||||
if argname in self.name2fixturedefs:
|
||||
working_set.update(self.name2fixturedefs[argname][-1].argnames)
|
||||
|
||||
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
|
||||
|
||||
|
||||
class FixtureRequest(FuncargnamesCompatAttr):
|
||||
@@ -582,7 +637,10 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
if scope == "function":
|
||||
# this might also be a non-function Item despite its attribute name
|
||||
return self._pyfuncitem
|
||||
node = get_scope_node(self._pyfuncitem, scope)
|
||||
if scope == "package":
|
||||
node = get_scope_package(self._pyfuncitem, self._fixturedef)
|
||||
else:
|
||||
node = get_scope_node(self._pyfuncitem, scope)
|
||||
if node is None and scope == "class":
|
||||
# fallback to function item itself
|
||||
node = self._pyfuncitem
|
||||
@@ -626,7 +684,7 @@ class ScopeMismatchError(Exception):
|
||||
"""
|
||||
|
||||
|
||||
scopes = "session module class function".split()
|
||||
scopes = "session package module class function".split()
|
||||
scopenum_function = scopes.index("function")
|
||||
|
||||
|
||||
@@ -698,7 +756,6 @@ class FixtureLookupError(LookupError):
|
||||
|
||||
|
||||
class FixtureLookupErrorRepr(TerminalRepr):
|
||||
|
||||
def __init__(self, filename, firstlineno, tblines, errorstring, argname):
|
||||
self.tblines = tblines
|
||||
self.errorstring = errorstring
|
||||
@@ -737,23 +794,26 @@ def call_fixture_func(fixturefunc, request, kwargs):
|
||||
if yieldctx:
|
||||
it = fixturefunc(**kwargs)
|
||||
res = next(it)
|
||||
|
||||
def teardown():
|
||||
try:
|
||||
next(it)
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
fail_fixturefunc(
|
||||
fixturefunc, "yield_fixture function has more than one 'yield'"
|
||||
)
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, it)
|
||||
request.addfinalizer(finalizer)
|
||||
else:
|
||||
res = fixturefunc(**kwargs)
|
||||
return res
|
||||
|
||||
|
||||
def _teardown_yield_fixture(fixturefunc, it):
|
||||
"""Executes the teardown of a fixture function by advancing the iterator after the
|
||||
yield and ensure the iteration ends (if not it means there is more than one yield in the function)"""
|
||||
try:
|
||||
next(it)
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
fail_fixturefunc(
|
||||
fixturefunc, "yield_fixture function has more than one 'yield'"
|
||||
)
|
||||
|
||||
|
||||
class FixtureDef(object):
|
||||
""" A container for a factory definition. """
|
||||
|
||||
@@ -837,21 +897,17 @@ class FixtureDef(object):
|
||||
return hook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<FixtureDef name=%r scope=%r baseid=%r >"
|
||||
% (self.argname, self.scope, self.baseid)
|
||||
return "<FixtureDef name=%r scope=%r baseid=%r >" % (
|
||||
self.argname,
|
||||
self.scope,
|
||||
self.baseid,
|
||||
)
|
||||
|
||||
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
""" Execution of fixture setup. """
|
||||
kwargs = {}
|
||||
for argname in fixturedef.argnames:
|
||||
fixdef = request._get_active_fixturedef(argname)
|
||||
result, arg_cache_key, exc = fixdef.cached_result
|
||||
request._check_scope(argname, request.scope, fixdef.scope)
|
||||
kwargs[argname] = result
|
||||
|
||||
def resolve_fixture_function(fixturedef, request):
|
||||
"""Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific
|
||||
instances and bound methods.
|
||||
"""
|
||||
fixturefunc = fixturedef.func
|
||||
if fixturedef.unittest:
|
||||
if request.instance is not None:
|
||||
@@ -865,6 +921,19 @@ def pytest_fixture_setup(fixturedef, request):
|
||||
fixturefunc = getimfunc(fixturedef.func)
|
||||
if fixturefunc != fixturedef.func:
|
||||
fixturefunc = fixturefunc.__get__(request.instance)
|
||||
return fixturefunc
|
||||
|
||||
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
""" Execution of fixture setup. """
|
||||
kwargs = {}
|
||||
for argname in fixturedef.argnames:
|
||||
fixdef = request._get_active_fixturedef(argname)
|
||||
result, arg_cache_key, exc = fixdef.cached_result
|
||||
request._check_scope(argname, request.scope, fixdef.scope)
|
||||
kwargs[argname] = result
|
||||
|
||||
fixturefunc = resolve_fixture_function(fixturedef, request)
|
||||
my_cache_key = request.param_index
|
||||
try:
|
||||
result = call_fixture_func(fixturefunc, request, kwargs)
|
||||
@@ -883,6 +952,41 @@ def _ensure_immutable_ids(ids):
|
||||
return tuple(ids)
|
||||
|
||||
|
||||
def wrap_function_to_warning_if_called_directly(function, fixture_marker):
|
||||
"""Wrap the given fixture function so we can issue warnings about it being called directly, instead of
|
||||
used as an argument in a test function.
|
||||
"""
|
||||
is_yield_function = is_generator(function)
|
||||
msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__)
|
||||
warning = RemovedInPytest4Warning(msg)
|
||||
|
||||
if is_yield_function:
|
||||
|
||||
@functools.wraps(function)
|
||||
def result(*args, **kwargs):
|
||||
__tracebackhide__ = True
|
||||
warnings.warn(warning, stacklevel=3)
|
||||
for x in function(*args, **kwargs):
|
||||
yield x
|
||||
|
||||
else:
|
||||
|
||||
@functools.wraps(function)
|
||||
def result(*args, **kwargs):
|
||||
__tracebackhide__ = True
|
||||
warnings.warn(warning, stacklevel=3)
|
||||
return function(*args, **kwargs)
|
||||
|
||||
if six.PY2:
|
||||
result.__wrapped__ = function
|
||||
|
||||
# keep reference to the original function in our own custom attribute so we don't unwrap
|
||||
# further than this point and lose useful wrappings like @mock.patch (#3774)
|
||||
result.__pytest_wrapped__ = _PytestWrapper(function)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class FixtureFunctionMarker(object):
|
||||
scope = attr.ib()
|
||||
@@ -900,6 +1004,8 @@ class FixtureFunctionMarker(object):
|
||||
"fixture is being applied more than once to the same function"
|
||||
)
|
||||
|
||||
function = wrap_function_to_warning_if_called_directly(function, self)
|
||||
|
||||
function._pytestfixturefunction = self
|
||||
return function
|
||||
|
||||
@@ -907,16 +1013,27 @@ class FixtureFunctionMarker(object):
|
||||
def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
|
||||
"""Decorator to mark a fixture factory function.
|
||||
|
||||
This decorator can be used (with or without parameters) to define a
|
||||
fixture function. The name of the fixture function can later be
|
||||
referenced to cause its invocation ahead of running tests: test
|
||||
modules or classes can use the pytest.mark.usefixtures(fixturename)
|
||||
marker. Test functions can directly use fixture names as input
|
||||
This decorator can be used, with or without parameters, to define a
|
||||
fixture function.
|
||||
|
||||
The name of the fixture function can later be referenced to cause its
|
||||
invocation ahead of running tests: test
|
||||
modules or classes can use the ``pytest.mark.usefixtures(fixturename)``
|
||||
marker.
|
||||
|
||||
Test functions can directly use fixture names as input
|
||||
arguments in which case the fixture instance returned from the fixture
|
||||
function will be injected.
|
||||
|
||||
Fixtures can provide their values to test functions using ``return`` or ``yield``
|
||||
statements. When using ``yield`` the code block after the ``yield`` statement is executed
|
||||
as teardown code regardless of the test outcome, and must yield exactly once.
|
||||
|
||||
:arg scope: the scope for which this fixture is shared, one of
|
||||
"function" (default), "class", "module" or "session".
|
||||
``"function"`` (default), ``"class"``, ``"module"``,
|
||||
``"package"`` or ``"session"``.
|
||||
|
||||
``"package"`` is considered **experimental** at this time.
|
||||
|
||||
:arg params: an optional list of parameters which will cause multiple
|
||||
invocations of the fixture function and all of the tests
|
||||
@@ -937,10 +1054,6 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
|
||||
to resolve this is to name the decorated function
|
||||
``fixture_<fixturename>`` and then use
|
||||
``@pytest.fixture(name='<fixturename>')``.
|
||||
|
||||
Fixtures can optionally provide their values to test functions using a ``yield`` statement,
|
||||
instead of ``return``. In this case, the code block after the ``yield`` statement is executed
|
||||
as teardown code regardless of the test outcome. A fixture function must yield exactly once.
|
||||
"""
|
||||
if callable(scope) and params is None and autouse is False:
|
||||
# direct decoration
|
||||
@@ -956,13 +1069,7 @@ def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=N
|
||||
.. deprecated:: 3.0
|
||||
Use :py:func:`pytest.fixture` directly instead.
|
||||
"""
|
||||
if callable(scope) and params is None and not autouse:
|
||||
# direct decoration
|
||||
return FixtureFunctionMarker("function", params, autouse, ids=ids, name=name)(
|
||||
scope
|
||||
)
|
||||
else:
|
||||
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
|
||||
return fixture(scope=scope, params=params, autouse=autouse, ids=ids, name=name)
|
||||
|
||||
|
||||
defaultfuncargprefixmarker = fixture()
|
||||
@@ -1035,11 +1142,12 @@ class FixtureManager(object):
|
||||
usefixtures = flatten(
|
||||
mark.args for mark in node.iter_markers(name="usefixtures")
|
||||
)
|
||||
initialnames = argnames
|
||||
initialnames = tuple(usefixtures) + initialnames
|
||||
initialnames = tuple(usefixtures) + argnames
|
||||
fm = node.session._fixturemanager
|
||||
names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames, node)
|
||||
return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs)
|
||||
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
|
||||
initialnames, node
|
||||
)
|
||||
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
nodeid = None
|
||||
@@ -1064,7 +1172,7 @@ class FixtureManager(object):
|
||||
if nodeid.startswith(baseid):
|
||||
if baseid:
|
||||
i = len(baseid)
|
||||
nextchar = nodeid[i:i + 1]
|
||||
nextchar = nodeid[i : i + 1]
|
||||
if nextchar and nextchar not in ":/":
|
||||
continue
|
||||
autousenames.extend(basenames)
|
||||
@@ -1087,6 +1195,12 @@ class FixtureManager(object):
|
||||
fixturenames_closure.append(arg)
|
||||
|
||||
merge(fixturenames)
|
||||
|
||||
# at this point, fixturenames_closure contains what we call "initialnames",
|
||||
# which is a set of fixturenames the function immediately requests. We
|
||||
# need to return it as well, so save this.
|
||||
initialnames = tuple(fixturenames_closure)
|
||||
|
||||
arg2fixturedefs = {}
|
||||
lastlen = -1
|
||||
while lastlen != len(fixturenames_closure):
|
||||
@@ -1108,7 +1222,7 @@ class FixtureManager(object):
|
||||
return fixturedefs[-1].scopenum
|
||||
|
||||
fixturenames_closure.sort(key=sort_by_scope)
|
||||
return fixturenames_closure, arg2fixturedefs
|
||||
return initialnames, fixturenames_closure, arg2fixturedefs
|
||||
|
||||
def pytest_generate_tests(self, metafunc):
|
||||
for argname in metafunc.fixturenames:
|
||||
@@ -1157,9 +1271,9 @@ class FixtureManager(object):
|
||||
# The attribute can be an arbitrary descriptor, so the attribute
|
||||
# access below can raise. safe_getatt() ignores such exceptions.
|
||||
obj = safe_getattr(holderobj, name, None)
|
||||
marker = getfixturemarker(obj)
|
||||
# fixture functions have a pytest_funcarg__ prefix (pre-2.3 style)
|
||||
# or are "@pytest.fixture" marked
|
||||
marker = getfixturemarker(obj)
|
||||
if marker is None:
|
||||
if not name.startswith(self._argprefix):
|
||||
continue
|
||||
@@ -1171,7 +1285,7 @@ class FixtureManager(object):
|
||||
self.config.warn(
|
||||
"C1", deprecated.FUNCARG_PREFIX.format(name=name), nodeid=nodeid
|
||||
)
|
||||
name = name[len(self._argprefix):]
|
||||
name = name[len(self._argprefix) :]
|
||||
elif not isinstance(marker, FixtureFunctionMarker):
|
||||
# magic globals with __getattr__ might have got us a wrong
|
||||
# fixture attribute
|
||||
@@ -1181,6 +1295,15 @@ class FixtureManager(object):
|
||||
name = marker.name
|
||||
assert not name.startswith(self._argprefix), FIXTURE_MSG.format(name)
|
||||
|
||||
# during fixture definition we wrap the original fixture function
|
||||
# to issue a warning if called directly, so here we unwrap it in order to not emit the warning
|
||||
# when pytest itself calls the fixture function
|
||||
if six.PY2 and unittest:
|
||||
# hack on Python 2 because of the unbound methods
|
||||
obj = get_real_func(obj)
|
||||
else:
|
||||
obj = get_real_method(obj, holderobj)
|
||||
|
||||
fixture_def = FixtureDef(
|
||||
self,
|
||||
nodeid,
|
||||
|
||||
@@ -149,7 +149,7 @@ def showhelp(config):
|
||||
type = "string"
|
||||
spec = "%s (%s)" % (name, type)
|
||||
line = " %-24s %s" % (spec, help)
|
||||
tw.line(line[:tw.fullwidth])
|
||||
tw.line(line[: tw.fullwidth])
|
||||
|
||||
tw.line()
|
||||
tw.line("environment variables:")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
||||
|
||||
from pluggy import HookspecMarker
|
||||
from .deprecated import PYTEST_NAMESPACE
|
||||
|
||||
|
||||
hookspec = HookspecMarker("pytest")
|
||||
|
||||
@@ -22,10 +24,9 @@ def pytest_addhooks(pluginmanager):
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
@hookspec(historic=True, warn_on_impl=PYTEST_NAMESPACE)
|
||||
def pytest_namespace():
|
||||
"""
|
||||
(**Deprecated**) this hook causes direct monkeypatching on pytest, its use is strongly discouraged
|
||||
return dict of name->object to be made globally available in
|
||||
the pytest namespace.
|
||||
|
||||
@@ -33,6 +34,19 @@ def pytest_namespace():
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
|
||||
.. warning::
|
||||
This hook has been **deprecated** and will be removed in pytest 4.0.
|
||||
|
||||
Plugins whose users depend on the current namespace functionality should prepare to migrate to a
|
||||
namespace they actually own.
|
||||
|
||||
To support the migration its suggested to trigger ``DeprecationWarnings`` for objects they put into the
|
||||
pytest namespace.
|
||||
|
||||
An stopgap measure to avoid the warning is to monkeypatch the ``pytest`` module, but just as the
|
||||
``pytest_namespace`` hook this should be seen as a temporary measure to be removed in future versions after
|
||||
an appropriate transition period.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ _py_ext_re = re.compile(r"\.py$")
|
||||
|
||||
|
||||
def bin_xml_escape(arg):
|
||||
|
||||
def repl(matchobj):
|
||||
i = ord(matchobj.group())
|
||||
if i <= 0xFF:
|
||||
@@ -67,7 +66,6 @@ def bin_xml_escape(arg):
|
||||
|
||||
|
||||
class _NodeReporter(object):
|
||||
|
||||
def __init__(self, nodeid, xml):
|
||||
|
||||
self.id = nodeid
|
||||
@@ -358,7 +356,6 @@ def mangle_test_address(address):
|
||||
|
||||
|
||||
class LogXML(object):
|
||||
|
||||
def __init__(self, logfile, prefix, suite_name="pytest", logging="no"):
|
||||
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
||||
self.logfile = os.path.normpath(os.path.abspath(logfile))
|
||||
@@ -544,9 +541,7 @@ class LogXML(object):
|
||||
skips=self.stats["skipped"],
|
||||
tests=numtests,
|
||||
time="%.3f" % suite_time_delta,
|
||||
).unicode(
|
||||
indent=0
|
||||
)
|
||||
).unicode(indent=0)
|
||||
)
|
||||
logfile.close()
|
||||
|
||||
|
||||
@@ -270,6 +270,22 @@ class LogCaptureFixture(object):
|
||||
"""
|
||||
return [(r.name, r.levelno, r.getMessage()) for r in self.records]
|
||||
|
||||
@property
|
||||
def messages(self):
|
||||
"""Returns a list of format-interpolated log messages.
|
||||
|
||||
Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list
|
||||
are all interpolated.
|
||||
Unlike 'text', which contains the output from the handler, log messages in this list are unadorned with
|
||||
levels, timestamps, etc, making exact comparisions more reliable.
|
||||
|
||||
Note that traceback or stack info (from :func:`logging.exception` or the `exc_info` or `stack_info` arguments
|
||||
to the logging functions) is not included, as this is added by the formatter in the handler.
|
||||
|
||||
.. versionadded:: 3.7
|
||||
"""
|
||||
return [r.getMessage() for r in self.records]
|
||||
|
||||
def clear(self):
|
||||
"""Reset the list of log records and the captured log text."""
|
||||
self.handler.reset()
|
||||
@@ -392,7 +408,9 @@ class LoggingPlugin(object):
|
||||
config, "log_file_date_format", "log_date_format"
|
||||
)
|
||||
# Each pytest runtests session will write to a clean logfile
|
||||
self.log_file_handler = logging.FileHandler(log_file, mode="w")
|
||||
self.log_file_handler = logging.FileHandler(
|
||||
log_file, mode="w", encoding="UTF-8"
|
||||
)
|
||||
log_file_formatter = logging.Formatter(
|
||||
log_file_format, datefmt=log_file_date_format
|
||||
)
|
||||
@@ -409,9 +427,7 @@ class LoggingPlugin(object):
|
||||
"""
|
||||
return self._config.getoption(
|
||||
"--log-cli-level"
|
||||
) is not None or self._config.getini(
|
||||
"log_cli"
|
||||
)
|
||||
) is not None or self._config.getini("log_cli")
|
||||
|
||||
@contextmanager
|
||||
def _runtest_for(self, item, when):
|
||||
|
||||
@@ -343,7 +343,6 @@ def _patched_find_module():
|
||||
|
||||
|
||||
class FSHookProxy(object):
|
||||
|
||||
def __init__(self, fspath, pm, remove_mods):
|
||||
self.fspath = fspath
|
||||
self.pm = pm
|
||||
@@ -361,6 +360,7 @@ class NoMatch(Exception):
|
||||
|
||||
class Interrupted(KeyboardInterrupt):
|
||||
""" signals an interrupted test run. """
|
||||
|
||||
__module__ = "builtins" # for py3
|
||||
|
||||
|
||||
@@ -383,6 +383,8 @@ class Session(nodes.FSCollector):
|
||||
self.trace = config.trace.root.get("collection")
|
||||
self._norecursepatterns = config.getini("norecursedirs")
|
||||
self.startdir = py.path.local()
|
||||
# Keep track of any collected nodes in here, so we don't duplicate fixtures
|
||||
self._node_cache = {}
|
||||
|
||||
self.config.pluginmanager.register(self, name="session")
|
||||
|
||||
@@ -480,19 +482,65 @@ class Session(nodes.FSCollector):
|
||||
self.trace.root.indent -= 1
|
||||
|
||||
def _collect(self, arg):
|
||||
from _pytest.python import Package
|
||||
|
||||
names = self._parsearg(arg)
|
||||
path = names.pop(0)
|
||||
if path.check(dir=1):
|
||||
argpath = names.pop(0)
|
||||
paths = []
|
||||
|
||||
root = self
|
||||
# Start with a Session root, and delve to argpath item (dir or file)
|
||||
# and stack all Packages found on the way.
|
||||
# No point in finding packages when collecting doctests
|
||||
if not self.config.option.doctestmodules:
|
||||
for parent in argpath.parts():
|
||||
pm = self.config.pluginmanager
|
||||
if pm._confcutdir and pm._confcutdir.relto(parent):
|
||||
continue
|
||||
|
||||
if parent.isdir():
|
||||
pkginit = parent.join("__init__.py")
|
||||
if pkginit.isfile():
|
||||
if pkginit in self._node_cache:
|
||||
root = self._node_cache[pkginit]
|
||||
else:
|
||||
col = root._collectfile(pkginit)
|
||||
if col:
|
||||
if isinstance(col[0], Package):
|
||||
root = col[0]
|
||||
self._node_cache[root.fspath] = root
|
||||
|
||||
# If it's a directory argument, recurse and look for any Subpackages.
|
||||
# Let the Package collector deal with subnodes, don't collect here.
|
||||
if argpath.check(dir=1):
|
||||
assert not names, "invalid arg %r" % (arg,)
|
||||
for path in path.visit(
|
||||
for path in argpath.visit(
|
||||
fil=lambda x: x.check(file=1), rec=self._recurse, bf=True, sort=True
|
||||
):
|
||||
for x in self._collectfile(path):
|
||||
yield x
|
||||
pkginit = path.dirpath().join("__init__.py")
|
||||
if pkginit.exists() and not any(x in pkginit.parts() for x in paths):
|
||||
for x in root._collectfile(pkginit):
|
||||
yield x
|
||||
paths.append(x.fspath.dirpath())
|
||||
|
||||
if not any(x in path.parts() for x in paths):
|
||||
for x in root._collectfile(path):
|
||||
if (type(x), x.fspath) in self._node_cache:
|
||||
yield self._node_cache[(type(x), x.fspath)]
|
||||
else:
|
||||
yield x
|
||||
self._node_cache[(type(x), x.fspath)] = x
|
||||
else:
|
||||
assert path.check(file=1)
|
||||
for x in self.matchnodes(self._collectfile(path), names):
|
||||
yield x
|
||||
assert argpath.check(file=1)
|
||||
|
||||
if argpath in self._node_cache:
|
||||
col = self._node_cache[argpath]
|
||||
else:
|
||||
col = root._collectfile(argpath)
|
||||
if col:
|
||||
self._node_cache[argpath] = col
|
||||
for y in self.matchnodes(col, names):
|
||||
yield y
|
||||
|
||||
def _collectfile(self, path):
|
||||
ihook = self.gethookproxy(path)
|
||||
@@ -577,7 +625,11 @@ class Session(nodes.FSCollector):
|
||||
resultnodes.append(node)
|
||||
continue
|
||||
assert isinstance(node, nodes.Collector)
|
||||
rep = collect_one_node(node)
|
||||
if node.nodeid in self._node_cache:
|
||||
rep = self._node_cache[node.nodeid]
|
||||
else:
|
||||
rep = collect_one_node(node)
|
||||
self._node_cache[node.nodeid] = rep
|
||||
if rep.passed:
|
||||
has_matched = False
|
||||
for x in rep.result:
|
||||
|
||||
@@ -21,7 +21,6 @@ def cached_eval(config, expr, d):
|
||||
|
||||
|
||||
class MarkEvaluator(object):
|
||||
|
||||
def __init__(self, item, name):
|
||||
self.item = item
|
||||
self._marks = None
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import inspect
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
from functools import reduce
|
||||
from operator import attrgetter
|
||||
|
||||
import attr
|
||||
|
||||
from ..deprecated import MARK_PARAMETERSET_UNPACKING, MARK_INFO_ATTRIBUTE
|
||||
from ..compat import NOTSET, getfslineno, MappingMixin
|
||||
from six.moves import map, reduce
|
||||
from six.moves import map
|
||||
|
||||
|
||||
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
|
||||
@@ -24,9 +25,10 @@ def alias(name, warning=None):
|
||||
|
||||
|
||||
def istestfunc(func):
|
||||
return hasattr(func, "__call__") and getattr(
|
||||
func, "__name__", "<lambda>"
|
||||
) != "<lambda>"
|
||||
return (
|
||||
hasattr(func, "__call__")
|
||||
and getattr(func, "__name__", "<lambda>") != "<lambda>"
|
||||
)
|
||||
|
||||
|
||||
def get_empty_parameterset_mark(config, argnames, func):
|
||||
@@ -39,18 +41,20 @@ def get_empty_parameterset_mark(config, argnames, func):
|
||||
raise LookupError(requested_mark)
|
||||
fs, lineno = getfslineno(func)
|
||||
reason = "got empty parameter set %r, function %s at %s:%d" % (
|
||||
argnames, func.__name__, fs, lineno
|
||||
argnames,
|
||||
func.__name__,
|
||||
fs,
|
||||
lineno,
|
||||
)
|
||||
return mark(reason=reason)
|
||||
|
||||
|
||||
class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||
|
||||
@classmethod
|
||||
def param(cls, *values, **kw):
|
||||
marks = kw.pop("marks", ())
|
||||
if isinstance(marks, MarkDecorator):
|
||||
marks = marks,
|
||||
marks = (marks,)
|
||||
else:
|
||||
assert isinstance(marks, (tuple, list, set))
|
||||
|
||||
@@ -87,7 +91,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||
argval = argval.args[-1]
|
||||
assert not isinstance(argval, ParameterSet)
|
||||
if legacy_force_tuple:
|
||||
argval = argval,
|
||||
argval = (argval,)
|
||||
|
||||
if newmarks:
|
||||
warnings.warn(MARK_PARAMETERSET_UNPACKING)
|
||||
@@ -107,7 +111,19 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||
]
|
||||
del argvalues
|
||||
|
||||
if not parameters:
|
||||
if parameters:
|
||||
# check all parameter sets have the correct number of values
|
||||
for param in parameters:
|
||||
if len(param.values) != len(argnames):
|
||||
raise ValueError(
|
||||
'In "parametrize" the number of values ({}) must be '
|
||||
"equal to the number of names ({})".format(
|
||||
param.values, argnames
|
||||
)
|
||||
)
|
||||
else:
|
||||
# empty parameter set (likely computed at runtime): create a single
|
||||
# parameter set with NOSET values, with the "empty parameter set" mark applied to it
|
||||
mark = get_empty_parameterset_mark(config, argnames, func)
|
||||
parameters.append(
|
||||
ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
|
||||
@@ -120,9 +136,9 @@ class Mark(object):
|
||||
#: name of the mark
|
||||
name = attr.ib(type=str)
|
||||
#: positional arguments of the mark decorator
|
||||
args = attr.ib(type="List[object]")
|
||||
args = attr.ib() # type: List[object]
|
||||
#: keyword arguments of the mark decorator
|
||||
kwargs = attr.ib(type="Dict[str, object]")
|
||||
kwargs = attr.ib() # type: Dict[str, object]
|
||||
|
||||
def combined_with(self, other):
|
||||
"""
|
||||
@@ -221,9 +237,18 @@ def get_unpacked_marks(obj):
|
||||
obtain the unpacked marks that are stored on an object
|
||||
"""
|
||||
mark_list = getattr(obj, "pytestmark", [])
|
||||
|
||||
if not isinstance(mark_list, list):
|
||||
mark_list = [mark_list]
|
||||
return normalize_mark_list(mark_list)
|
||||
|
||||
|
||||
def normalize_mark_list(mark_list):
|
||||
"""
|
||||
normalizes marker decorating helpers to mark objects
|
||||
|
||||
:type mark_list: List[Union[Mark, Markdecorator]]
|
||||
:rtype: List[Mark]
|
||||
"""
|
||||
return [getattr(mark, "mark", mark) for mark in mark_list] # unpack MarkDecorator
|
||||
|
||||
|
||||
@@ -246,7 +271,7 @@ def store_legacy_markinfo(func, mark):
|
||||
if holder is None:
|
||||
holder = MarkInfo.for_mark(mark)
|
||||
setattr(func, mark.name, holder)
|
||||
else:
|
||||
elif isinstance(holder, MarkInfo):
|
||||
holder.add_mark(mark)
|
||||
|
||||
|
||||
@@ -281,7 +306,7 @@ def _marked(func, mark):
|
||||
class MarkInfo(object):
|
||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||
|
||||
_marks = attr.ib(convert=list)
|
||||
_marks = attr.ib(converter=list)
|
||||
|
||||
@_marks.validator
|
||||
def validate_marks(self, attribute, value):
|
||||
@@ -332,6 +357,7 @@ class MarkGenerator(object):
|
||||
|
||||
will set a 'slowtest' :class:`MarkInfo` object
|
||||
on the ``test_function`` object. """
|
||||
|
||||
_config = None
|
||||
|
||||
def __getattr__(self, name):
|
||||
@@ -361,7 +387,6 @@ MARK_GEN = MarkGenerator()
|
||||
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
@@ -408,6 +433,7 @@ class NodeMarkers(object):
|
||||
unstable api
|
||||
|
||||
"""
|
||||
|
||||
own_markers = attr.ib(default=attr.Factory(list))
|
||||
|
||||
def update(self, add_markers):
|
||||
|
||||
@@ -22,7 +22,7 @@ def monkeypatch():
|
||||
monkeypatch.setitem(mapping, name, value)
|
||||
monkeypatch.delitem(obj, name, raising=True)
|
||||
monkeypatch.setenv(name, value, prepend=False)
|
||||
monkeypatch.delenv(name, value, raising=True)
|
||||
monkeypatch.delenv(name, raising=True)
|
||||
monkeypatch.syspath_prepend(path)
|
||||
monkeypatch.chdir(path)
|
||||
|
||||
@@ -86,7 +86,6 @@ def derive_importpath(import_path, raising):
|
||||
|
||||
|
||||
class Notset(object):
|
||||
|
||||
def __repr__(self):
|
||||
return "<notset>"
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def ischildnode(baseid, nodeid):
|
||||
node_parts = _splitnode(nodeid)
|
||||
if len(node_parts) < len(base_parts):
|
||||
return False
|
||||
return node_parts[:len(base_parts)] == base_parts
|
||||
return node_parts[: len(base_parts)] == base_parts
|
||||
|
||||
|
||||
@attr.s
|
||||
@@ -173,10 +173,13 @@ class Node(object):
|
||||
chain.reverse()
|
||||
return chain
|
||||
|
||||
def add_marker(self, marker):
|
||||
def add_marker(self, marker, append=True):
|
||||
"""dynamically add a marker object to the node.
|
||||
|
||||
:type marker: str or pytest.mark.*
|
||||
:type marker: ``str`` or ``pytest.mark.*`` object
|
||||
:param marker:
|
||||
``append=True`` whether to append the marker,
|
||||
if ``False`` insert at position ``0``.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator, MARK_GEN
|
||||
|
||||
@@ -185,7 +188,10 @@ class Node(object):
|
||||
elif not isinstance(marker, MarkDecorator):
|
||||
raise ValueError("is not a string or pytest.mark.* Marker")
|
||||
self.keywords[marker.name] = marker
|
||||
self.own_markers.append(marker.mark)
|
||||
if append:
|
||||
self.own_markers.append(marker.mark)
|
||||
else:
|
||||
self.own_markers.insert(0, marker.mark)
|
||||
|
||||
def iter_markers(self, name=None):
|
||||
"""
|
||||
@@ -281,6 +287,11 @@ class Node(object):
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
if self.config.option.verbose > 1:
|
||||
truncate_locals = False
|
||||
else:
|
||||
truncate_locals = True
|
||||
|
||||
try:
|
||||
os.getcwd()
|
||||
abspath = False
|
||||
@@ -293,6 +304,7 @@ class Node(object):
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style,
|
||||
tbfilter=tbfilter,
|
||||
truncate_locals=truncate_locals,
|
||||
)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
@@ -335,7 +347,6 @@ def _check_initialpaths_for_relpath(session, fspath):
|
||||
|
||||
|
||||
class FSCollector(Collector):
|
||||
|
||||
def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
|
||||
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
|
||||
name = fspath.basename
|
||||
@@ -353,7 +364,7 @@ class FSCollector(Collector):
|
||||
|
||||
if not nodeid:
|
||||
nodeid = _check_initialpaths_for_relpath(session, fspath)
|
||||
if os.sep != SEP:
|
||||
if nodeid and os.sep != SEP:
|
||||
nodeid = nodeid.replace(os.sep, SEP)
|
||||
|
||||
super(FSCollector, self).__init__(
|
||||
@@ -369,6 +380,7 @@ class Item(Node):
|
||||
""" a basic test invocation item. Note that for a single function
|
||||
there might be multiple test invocation items.
|
||||
"""
|
||||
|
||||
nextitem = None
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None, nodeid=None):
|
||||
|
||||
@@ -43,6 +43,7 @@ class Skipped(OutcomeException):
|
||||
|
||||
class Failed(OutcomeException):
|
||||
""" raised from an explicit call to pytest.fail() """
|
||||
|
||||
__module__ = "builtins"
|
||||
|
||||
|
||||
|
||||
13
src/_pytest/paths.py
Normal file
13
src/_pytest/paths.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from .compat import Path
|
||||
from os.path import expanduser, expandvars, isabs
|
||||
|
||||
|
||||
def resolve_from_str(input, root):
|
||||
assert not isinstance(input, Path), "would break on py2"
|
||||
root = Path(root)
|
||||
input = expanduser(input)
|
||||
input = expandvars(input)
|
||||
if isabs(input):
|
||||
return Path(input)
|
||||
else:
|
||||
return root.joinpath(input)
|
||||
@@ -21,12 +21,7 @@ import py
|
||||
import pytest
|
||||
from _pytest.main import Session, EXIT_OK
|
||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||
|
||||
|
||||
PYTEST_FULLPATH = os.path.abspath(pytest.__file__.rstrip("oc")).replace(
|
||||
"$py.class", ".py"
|
||||
)
|
||||
|
||||
from _pytest.compat import Path
|
||||
|
||||
IGNORE_PAM = [ # filenames added when obtaining details about the current user
|
||||
u"/var/lib/sss/mc/passwd"
|
||||
@@ -53,6 +48,10 @@ def pytest_addoption(parser):
|
||||
),
|
||||
)
|
||||
|
||||
parser.addini(
|
||||
"pytester_example_dir", help="directory to take the pytester example files from"
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.getvalue("lsof"):
|
||||
@@ -62,7 +61,6 @@ def pytest_configure(config):
|
||||
|
||||
|
||||
class LsofFdLeakChecker(object):
|
||||
|
||||
def get_open_files(self):
|
||||
out = self._exec_lsof()
|
||||
open_files = self._parse_lsof_output(out)
|
||||
@@ -73,7 +71,6 @@ class LsofFdLeakChecker(object):
|
||||
return py.process.cmdexec("lsof -Ffn0 -p %d" % pid)
|
||||
|
||||
def _parse_lsof_output(self, out):
|
||||
|
||||
def isopen(line):
|
||||
return line.startswith("f") and (
|
||||
"deleted" not in line
|
||||
@@ -195,7 +192,6 @@ def _pytest(request):
|
||||
|
||||
|
||||
class PytestArg(object):
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
@@ -211,7 +207,6 @@ def get_public_names(values):
|
||||
|
||||
|
||||
class ParsedCall(object):
|
||||
|
||||
def __init__(self, name, kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
self._name = name
|
||||
@@ -423,13 +418,12 @@ class RunResult(object):
|
||||
"failed": d.get("failed", 0),
|
||||
"error": d.get("error", 0),
|
||||
}
|
||||
assert (
|
||||
obtained == dict(passed=passed, skipped=skipped, failed=failed, error=error)
|
||||
assert obtained == dict(
|
||||
passed=passed, skipped=skipped, failed=failed, error=error
|
||||
)
|
||||
|
||||
|
||||
class CwdSnapshot(object):
|
||||
|
||||
def __init__(self):
|
||||
self.__saved = os.getcwd()
|
||||
|
||||
@@ -438,7 +432,6 @@ class CwdSnapshot(object):
|
||||
|
||||
|
||||
class SysModulesSnapshot(object):
|
||||
|
||||
def __init__(self, preserve=None):
|
||||
self.__preserve = preserve
|
||||
self.__saved = dict(sys.modules)
|
||||
@@ -453,7 +446,6 @@ class SysModulesSnapshot(object):
|
||||
|
||||
|
||||
class SysPathsSnapshot(object):
|
||||
|
||||
def __init__(self):
|
||||
self.__saved = list(sys.path), list(sys.meta_path)
|
||||
|
||||
@@ -635,6 +627,48 @@ class Testdir(object):
|
||||
p.ensure("__init__.py")
|
||||
return p
|
||||
|
||||
def copy_example(self, name=None):
|
||||
from . import experiments
|
||||
import warnings
|
||||
|
||||
warnings.warn(experiments.PYTESTER_COPY_EXAMPLE, stacklevel=2)
|
||||
example_dir = self.request.config.getini("pytester_example_dir")
|
||||
if example_dir is None:
|
||||
raise ValueError("pytester_example_dir is unset, can't copy examples")
|
||||
example_dir = self.request.config.rootdir.join(example_dir)
|
||||
|
||||
for extra_element in self.request.node.iter_markers("pytester_example_path"):
|
||||
assert extra_element.args
|
||||
example_dir = example_dir.join(*extra_element.args)
|
||||
|
||||
if name is None:
|
||||
func_name = self.request.function.__name__
|
||||
maybe_dir = example_dir / func_name
|
||||
maybe_file = example_dir / (func_name + ".py")
|
||||
|
||||
if maybe_dir.isdir():
|
||||
example_path = maybe_dir
|
||||
elif maybe_file.isfile():
|
||||
example_path = maybe_file
|
||||
else:
|
||||
raise LookupError(
|
||||
"{} cant be found as module or package in {}".format(
|
||||
func_name, example_dir.bestrelpath(self.request.confg.rootdir)
|
||||
)
|
||||
)
|
||||
else:
|
||||
example_path = example_dir.join(name)
|
||||
|
||||
if example_path.isdir() and not example_path.join("__init__.py").isfile():
|
||||
example_path.copy(self.tmpdir)
|
||||
return self.tmpdir
|
||||
elif example_path.isfile():
|
||||
result = self.tmpdir.join(example_path.basename)
|
||||
example_path.copy(result)
|
||||
return result
|
||||
else:
|
||||
raise LookupError("example is not found as a file or directory")
|
||||
|
||||
Session = Session
|
||||
|
||||
def getnode(self, config, arg):
|
||||
@@ -778,7 +812,6 @@ class Testdir(object):
|
||||
rec = []
|
||||
|
||||
class Collect(object):
|
||||
|
||||
def pytest_configure(x, config):
|
||||
rec.append(self.make_hook_recorder(config.pluginmanager))
|
||||
|
||||
@@ -910,8 +943,10 @@ class Testdir(object):
|
||||
for item in items:
|
||||
if item.name == funcname:
|
||||
return item
|
||||
assert 0, (
|
||||
"%r item not found in module:\n%s\nitems: %s" % (funcname, source, items)
|
||||
assert 0, "%r item not found in module:\n%s\nitems: %s" % (
|
||||
funcname,
|
||||
source,
|
||||
items,
|
||||
)
|
||||
|
||||
def getitems(self, source):
|
||||
@@ -940,14 +975,16 @@ class Testdir(object):
|
||||
same directory to ensure it is a package
|
||||
|
||||
"""
|
||||
kw = {self.request.function.__name__: Source(source).strip()}
|
||||
path = self.makepyfile(**kw)
|
||||
if isinstance(source, Path):
|
||||
path = self.tmpdir.join(str(source))
|
||||
assert not withinit, "not supported for paths"
|
||||
else:
|
||||
kw = {self.request.function.__name__: Source(source).strip()}
|
||||
path = self.makepyfile(**kw)
|
||||
if withinit:
|
||||
self.makepyfile(__init__="#")
|
||||
self.config = config = self.parseconfigure(path, *configargs)
|
||||
node = self.getnode(config, path)
|
||||
|
||||
return node
|
||||
return self.getnode(config, path)
|
||||
|
||||
def collect_by_name(self, modcol, name):
|
||||
"""Return the collection node for name from the module collection.
|
||||
@@ -1035,9 +1072,7 @@ class Testdir(object):
|
||||
print("couldn't print to %s because of encoding" % (fp,))
|
||||
|
||||
def _getpytestargs(self):
|
||||
# we cannot use `(sys.executable, script)` because on Windows the
|
||||
# script is e.g. `pytest.exe`
|
||||
return (sys.executable, PYTEST_FULLPATH) # noqa
|
||||
return (sys.executable, "-mpytest")
|
||||
|
||||
def runpython(self, script):
|
||||
"""Run a python script using sys.executable as interpreter.
|
||||
@@ -1115,7 +1150,6 @@ def getdecoded(out):
|
||||
|
||||
|
||||
class LineComp(object):
|
||||
|
||||
def __init__(self):
|
||||
self.stringio = py.io.TextIO()
|
||||
|
||||
@@ -1202,7 +1236,7 @@ class LineMatcher(object):
|
||||
"""
|
||||
for i, line in enumerate(self.lines):
|
||||
if fnline == line or fnmatch(line, fnline):
|
||||
return self.lines[i + 1:]
|
||||
return self.lines[i + 1 :]
|
||||
raise ValueError("line %r not found in output" % fnline)
|
||||
|
||||
def _log(self, *args):
|
||||
|
||||
@@ -8,11 +8,11 @@ import os
|
||||
import collections
|
||||
import warnings
|
||||
from textwrap import dedent
|
||||
from itertools import count
|
||||
|
||||
|
||||
import py
|
||||
import six
|
||||
from _pytest.main import FSHookProxy
|
||||
from _pytest.mark import MarkerError
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
@@ -39,7 +39,11 @@ from _pytest.compat import (
|
||||
get_default_arg_names,
|
||||
)
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.mark.structures import transfer_markers, get_unpacked_marks
|
||||
from _pytest.mark.structures import (
|
||||
transfer_markers,
|
||||
get_unpacked_marks,
|
||||
normalize_mark_list,
|
||||
)
|
||||
|
||||
|
||||
# relative paths that we use to filter traceback entries from appearing to the user;
|
||||
@@ -69,13 +73,12 @@ def filter_traceback(entry):
|
||||
# entry.path might point to a non-existing file, in which case it will
|
||||
# also return a str object. see #1133
|
||||
p = py.path.local(entry.path)
|
||||
return not p.relto(_pluggy_dir) and not p.relto(_pytest_dir) and not p.relto(
|
||||
_py_dir
|
||||
return (
|
||||
not p.relto(_pluggy_dir) and not p.relto(_pytest_dir) and not p.relto(_py_dir)
|
||||
)
|
||||
|
||||
|
||||
def pyobj_property(name):
|
||||
|
||||
def get(self):
|
||||
node = self.getparent(getattr(__import__("pytest"), name))
|
||||
if node is not None:
|
||||
@@ -198,7 +201,7 @@ def pytest_collect_file(path, parent):
|
||||
ext = path.ext
|
||||
if ext == ".py":
|
||||
if not parent.session.isinitpath(path):
|
||||
for pat in parent.config.getini("python_files"):
|
||||
for pat in parent.config.getini("python_files") + ["__init__.py"]:
|
||||
if path.fnmatch(pat):
|
||||
break
|
||||
else:
|
||||
@@ -208,6 +211,8 @@ def pytest_collect_file(path, parent):
|
||||
|
||||
|
||||
def pytest_pycollect_makemodule(path, parent):
|
||||
if path.basename == "__init__.py":
|
||||
return Package(path, parent)
|
||||
return Module(path, parent)
|
||||
|
||||
|
||||
@@ -258,7 +263,6 @@ class PyobjMixin(PyobjContext):
|
||||
super(PyobjMixin, self).__init__(*k, **kw)
|
||||
|
||||
def obj():
|
||||
|
||||
def fget(self):
|
||||
obj = getattr(self, "_obj", None)
|
||||
if obj is None:
|
||||
@@ -320,7 +324,6 @@ class PyobjMixin(PyobjContext):
|
||||
|
||||
|
||||
class PyCollector(PyobjMixin, nodes.Collector):
|
||||
|
||||
def funcnamefilter(self, name):
|
||||
return self._matches_prefix_or_glob_option("python_functions", name)
|
||||
|
||||
@@ -438,6 +441,11 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
# add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
|
||||
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
||||
|
||||
# add_funcarg_pseudo_fixture_def may have shadowed some fixtures
|
||||
# with direct parametrization, so make sure we update what the
|
||||
# function really needs.
|
||||
fixtureinfo.prune_dependency_tree()
|
||||
|
||||
for callspec in metafunc._calls:
|
||||
subname = "%s[%s]" % (name, callspec.id)
|
||||
yield Function(
|
||||
@@ -487,9 +495,11 @@ class Module(nodes.File, PyCollector):
|
||||
exc_info = ExceptionInfo()
|
||||
if self.config.getoption("verbose") < 2:
|
||||
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
|
||||
exc_repr = exc_info.getrepr(
|
||||
style="short"
|
||||
) if exc_info.traceback else exc_info.exconly()
|
||||
exc_repr = (
|
||||
exc_info.getrepr(style="short")
|
||||
if exc_info.traceback
|
||||
else exc_info.exconly()
|
||||
)
|
||||
formatted_tb = safe_str(exc_repr)
|
||||
raise self.CollectError(
|
||||
"ImportError while importing test module '{fspath}'.\n"
|
||||
@@ -523,6 +533,76 @@ class Module(nodes.File, PyCollector):
|
||||
self.addfinalizer(teardown_module)
|
||||
|
||||
|
||||
class Package(Module):
|
||||
def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
|
||||
session = parent.session
|
||||
nodes.FSCollector.__init__(
|
||||
self, fspath, parent=parent, config=config, session=session, nodeid=nodeid
|
||||
)
|
||||
self.name = fspath.dirname
|
||||
self.trace = session.trace
|
||||
self._norecursepatterns = session._norecursepatterns
|
||||
self.fspath = fspath
|
||||
|
||||
def _recurse(self, path):
|
||||
ihook = self.gethookproxy(path.dirpath())
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
return False
|
||||
for pat in self._norecursepatterns:
|
||||
if path.check(fnmatch=pat):
|
||||
return False
|
||||
ihook = self.gethookproxy(path)
|
||||
ihook.pytest_collect_directory(path=path, parent=self)
|
||||
return True
|
||||
|
||||
def gethookproxy(self, fspath):
|
||||
# check if we have the common case of running
|
||||
# hooks with all conftest.py filesall conftest.py
|
||||
pm = self.config.pluginmanager
|
||||
my_conftestmodules = pm._getconftestmodules(fspath)
|
||||
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
||||
if remove_mods:
|
||||
# one or more conftests are not in use at this fspath
|
||||
proxy = FSHookProxy(fspath, pm, remove_mods)
|
||||
else:
|
||||
# all plugis are active for this fspath
|
||||
proxy = self.config.hook
|
||||
return proxy
|
||||
|
||||
def _collectfile(self, path):
|
||||
ihook = self.gethookproxy(path)
|
||||
if not self.isinitpath(path):
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
return ()
|
||||
return ihook.pytest_collect_file(path=path, parent=self)
|
||||
|
||||
def isinitpath(self, path):
|
||||
return path in self.session._initialpaths
|
||||
|
||||
def collect(self):
|
||||
# XXX: HACK!
|
||||
# Before starting to collect any files from this package we need
|
||||
# to cleanup the duplicate paths added by the session's collect().
|
||||
# Proper fix is to not track these as duplicates in the first place.
|
||||
for path in list(self.session.config.pluginmanager._duplicatepaths):
|
||||
# if path.parts()[:len(self.fspath.dirpath().parts())] == self.fspath.dirpath().parts():
|
||||
if path.dirname.startswith(self.name):
|
||||
self.session.config.pluginmanager._duplicatepaths.remove(path)
|
||||
|
||||
this_path = self.fspath.dirpath()
|
||||
pkg_prefix = None
|
||||
for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
|
||||
# we will visit our own __init__.py file, in which case we skip it
|
||||
if path.basename == "__init__.py" and path.dirpath() == this_path:
|
||||
continue
|
||||
if pkg_prefix and pkg_prefix in path.parts():
|
||||
continue
|
||||
for x in self._collectfile(path):
|
||||
yield x
|
||||
if isinstance(x, Package):
|
||||
pkg_prefix = path.dirpath()
|
||||
|
||||
|
||||
def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
|
||||
"""
|
||||
Return a callable to perform xunit-style setup or teardown if
|
||||
@@ -673,7 +753,6 @@ class FunctionMixin(PyobjMixin):
|
||||
|
||||
|
||||
class Generator(FunctionMixin, PyCollector):
|
||||
|
||||
def collect(self):
|
||||
# test generators are seen as collectors but they also
|
||||
# invoke setup/teardown on popular request
|
||||
@@ -728,20 +807,18 @@ def hasnew(obj):
|
||||
|
||||
|
||||
class CallSpec2(object):
|
||||
|
||||
def __init__(self, metafunc):
|
||||
self.metafunc = metafunc
|
||||
self.funcargs = {}
|
||||
self._idlist = []
|
||||
self.params = {}
|
||||
self._globalid = NOTSET
|
||||
self._globalid_args = set()
|
||||
self._globalparam = NOTSET
|
||||
self._arg2scopenum = {} # used for sorting parametrized resources
|
||||
self.marks = []
|
||||
self.indices = {}
|
||||
|
||||
def copy(self, metafunc):
|
||||
def copy(self):
|
||||
cs = CallSpec2(self.metafunc)
|
||||
cs.funcargs.update(self.funcargs)
|
||||
cs.params.update(self.params)
|
||||
@@ -750,7 +827,6 @@ class CallSpec2(object):
|
||||
cs._arg2scopenum.update(self._arg2scopenum)
|
||||
cs._idlist = list(self._idlist)
|
||||
cs._globalid = self._globalid
|
||||
cs._globalid_args = self._globalid_args
|
||||
cs._globalparam = self._globalparam
|
||||
return cs
|
||||
|
||||
@@ -778,7 +854,7 @@ class CallSpec2(object):
|
||||
self.indices[arg] = param_index
|
||||
self._arg2scopenum[arg] = scopenum
|
||||
self._idlist.append(id)
|
||||
self.marks.extend(marks)
|
||||
self.marks.extend(normalize_mark_list(marks))
|
||||
|
||||
def setall(self, funcargs, id, param):
|
||||
for x in funcargs:
|
||||
@@ -802,12 +878,13 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
"""
|
||||
|
||||
def __init__(self, definition, fixtureinfo, config, cls=None, module=None):
|
||||
#: access to the :class:`_pytest.config.Config` object for the test session
|
||||
assert (
|
||||
isinstance(definition, FunctionDefinition)
|
||||
or type(definition).__name__ == "DefinitionMock"
|
||||
)
|
||||
self.definition = definition
|
||||
|
||||
#: access to the :class:`_pytest.config.Config` object for the test session
|
||||
self.config = config
|
||||
|
||||
#: the module object where the test function is defined in.
|
||||
@@ -866,46 +943,54 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
"""
|
||||
from _pytest.fixtures import scope2index
|
||||
from _pytest.mark import ParameterSet
|
||||
from py.io import saferepr
|
||||
|
||||
argnames, parameters = ParameterSet._for_parametrize(
|
||||
argnames, argvalues, self.function, self.config
|
||||
)
|
||||
del argvalues
|
||||
default_arg_names = set(get_default_arg_names(self.function))
|
||||
|
||||
if scope is None:
|
||||
scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
|
||||
|
||||
scopenum = scope2index(scope, descr="call to {}".format(self.parametrize))
|
||||
valtypes = {}
|
||||
for arg in argnames:
|
||||
if arg not in self.fixturenames:
|
||||
if arg in default_arg_names:
|
||||
raise ValueError(
|
||||
"%r already takes an argument %r with a default value"
|
||||
% (self.function, arg)
|
||||
)
|
||||
else:
|
||||
if isinstance(indirect, (tuple, list)):
|
||||
name = "fixture" if arg in indirect else "argument"
|
||||
else:
|
||||
name = "fixture" if indirect else "argument"
|
||||
raise ValueError("%r uses no %s %r" % (self.function, name, arg))
|
||||
self._validate_if_using_arg_names(argnames, indirect)
|
||||
|
||||
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
|
||||
|
||||
ids = self._resolve_arg_ids(argnames, ids, parameters)
|
||||
|
||||
scopenum = scope2index(scope, descr="call to {}".format(self.parametrize))
|
||||
|
||||
# create the new calls: if we are parametrize() multiple times (by applying the decorator
|
||||
# more than once) then we accumulate those calls generating the cartesian product
|
||||
# of all calls
|
||||
newcalls = []
|
||||
for callspec in self._calls or [CallSpec2(self)]:
|
||||
for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
|
||||
newcallspec = callspec.copy()
|
||||
newcallspec.setmulti2(
|
||||
arg_values_types,
|
||||
argnames,
|
||||
param_set.values,
|
||||
param_id,
|
||||
param_set.marks,
|
||||
scopenum,
|
||||
param_index,
|
||||
)
|
||||
newcalls.append(newcallspec)
|
||||
self._calls = newcalls
|
||||
|
||||
def _resolve_arg_ids(self, argnames, ids, parameters):
|
||||
"""Resolves the actual ids for the given argnames, based on the ``ids`` parameter given
|
||||
to ``parametrize``.
|
||||
|
||||
:param List[str] argnames: list of argument names passed to ``parametrize()``.
|
||||
:param ids: the ids parameter of the parametrized call (see docs).
|
||||
:param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``.
|
||||
:rtype: List[str]
|
||||
:return: the list of ids for each argname given
|
||||
"""
|
||||
from py.io import saferepr
|
||||
|
||||
if indirect is True:
|
||||
valtypes = dict.fromkeys(argnames, "params")
|
||||
elif indirect is False:
|
||||
valtypes = dict.fromkeys(argnames, "funcargs")
|
||||
elif isinstance(indirect, (tuple, list)):
|
||||
valtypes = dict.fromkeys(argnames, "funcargs")
|
||||
for arg in indirect:
|
||||
if arg not in argnames:
|
||||
raise ValueError(
|
||||
"indirect given to %r: fixture %r doesn't exist"
|
||||
% (self.function, arg)
|
||||
)
|
||||
valtypes[arg] = "params"
|
||||
idfn = None
|
||||
if callable(ids):
|
||||
idfn = ids
|
||||
@@ -922,29 +1007,57 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
msg % (saferepr(id_value), type(id_value).__name__)
|
||||
)
|
||||
ids = idmaker(argnames, parameters, idfn, ids, self.config)
|
||||
newcalls = []
|
||||
for callspec in self._calls or [CallSpec2(self)]:
|
||||
elements = zip(ids, parameters, count())
|
||||
for a_id, param, param_index in elements:
|
||||
if len(param.values) != len(argnames):
|
||||
return ids
|
||||
|
||||
def _resolve_arg_value_types(self, argnames, indirect):
|
||||
"""Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg"
|
||||
to the function, based on the ``indirect`` parameter of the parametrized() call.
|
||||
|
||||
:param List[str] argnames: list of argument names passed to ``parametrize()``.
|
||||
:param indirect: same ``indirect`` parameter of ``parametrize()``.
|
||||
:rtype: Dict[str, str]
|
||||
A dict mapping each arg name to either:
|
||||
* "params" if the argname should be the parameter of a fixture of the same name.
|
||||
* "funcargs" if the argname should be a parameter to the parametrized test function.
|
||||
"""
|
||||
valtypes = {}
|
||||
if indirect is True:
|
||||
valtypes = dict.fromkeys(argnames, "params")
|
||||
elif indirect is False:
|
||||
valtypes = dict.fromkeys(argnames, "funcargs")
|
||||
elif isinstance(indirect, (tuple, list)):
|
||||
valtypes = dict.fromkeys(argnames, "funcargs")
|
||||
for arg in indirect:
|
||||
if arg not in argnames:
|
||||
raise ValueError(
|
||||
'In "parametrize" the number of values ({}) must be '
|
||||
"equal to the number of names ({})".format(
|
||||
param.values, argnames
|
||||
)
|
||||
"indirect given to %r: fixture %r doesn't exist"
|
||||
% (self.function, arg)
|
||||
)
|
||||
newcallspec = callspec.copy(self)
|
||||
newcallspec.setmulti2(
|
||||
valtypes,
|
||||
argnames,
|
||||
param.values,
|
||||
a_id,
|
||||
param.marks,
|
||||
scopenum,
|
||||
param_index,
|
||||
)
|
||||
newcalls.append(newcallspec)
|
||||
self._calls = newcalls
|
||||
valtypes[arg] = "params"
|
||||
return valtypes
|
||||
|
||||
def _validate_if_using_arg_names(self, argnames, indirect):
|
||||
"""
|
||||
Check if all argnames are being used, by default values, or directly/indirectly.
|
||||
|
||||
:param List[str] argnames: list of argument names passed to ``parametrize()``.
|
||||
:param indirect: same ``indirect`` parameter of ``parametrize()``.
|
||||
:raise ValueError: if validation fails.
|
||||
"""
|
||||
default_arg_names = set(get_default_arg_names(self.function))
|
||||
for arg in argnames:
|
||||
if arg not in self.fixturenames:
|
||||
if arg in default_arg_names:
|
||||
raise ValueError(
|
||||
"%r already takes an argument %r with a default value"
|
||||
% (self.function, arg)
|
||||
)
|
||||
else:
|
||||
if isinstance(indirect, (tuple, list)):
|
||||
name = "fixture" if arg in indirect else "argument"
|
||||
else:
|
||||
name = "fixture" if indirect else "argument"
|
||||
raise ValueError("%r uses no %s %r" % (self.function, name, arg))
|
||||
|
||||
def addcall(self, funcargs=None, id=NOTSET, param=NOTSET):
|
||||
""" Add a new call to the underlying test function during the collection phase of a test run.
|
||||
@@ -1004,9 +1117,9 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
|
||||
from _pytest.fixtures import scopes
|
||||
|
||||
indirect_as_list = isinstance(indirect, (list, tuple))
|
||||
all_arguments_are_fixtures = indirect is True or indirect_as_list and len(
|
||||
indirect
|
||||
) == argnames
|
||||
all_arguments_are_fixtures = (
|
||||
indirect is True or indirect_as_list and len(indirect) == argnames
|
||||
)
|
||||
if all_arguments_are_fixtures:
|
||||
fixturedefs = arg2fixturedefs or {}
|
||||
used_scopes = [fixturedef[0].scope for name, fixturedef in fixturedefs.items()]
|
||||
@@ -1028,8 +1141,9 @@ def _idval(val, argname, idx, idfn, config=None):
|
||||
# See issue https://github.com/pytest-dev/pytest/issues/2169
|
||||
import warnings
|
||||
|
||||
msg = "Raised while trying to determine id of parameter %s at position %d." % (
|
||||
argname, idx
|
||||
msg = (
|
||||
"Raised while trying to determine id of parameter %s at position %d."
|
||||
% (argname, idx)
|
||||
)
|
||||
msg += "\nUpdate your code as this will raise an error in pytest-4.0."
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
@@ -1224,6 +1338,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
||||
""" a Function Item is responsible for setting up and executing a
|
||||
Python test function.
|
||||
"""
|
||||
|
||||
_genid = None
|
||||
# disable since functions handle it themselfes
|
||||
_ALLOW_MARKERS = False
|
||||
@@ -1257,7 +1372,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
||||
# feel free to cry, this was broken for years before
|
||||
# and keywords cant fix it per design
|
||||
self.keywords[mark.name] = mark
|
||||
self.own_markers.extend(callspec.marks)
|
||||
self.own_markers.extend(normalize_mark_list(callspec.marks))
|
||||
if keywords:
|
||||
self.keywords.update(keywords)
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import math
|
||||
import pprint
|
||||
import sys
|
||||
from numbers import Number
|
||||
from decimal import Decimal
|
||||
|
||||
import py
|
||||
from six.moves import zip, filterfalse
|
||||
@@ -30,6 +33,15 @@ def _cmp_raises_type_error(self, other):
|
||||
)
|
||||
|
||||
|
||||
def _non_numeric_type_error(value, at):
|
||||
at_str = " at {}".format(at) if at else ""
|
||||
return TypeError(
|
||||
"cannot make approximate comparisons to non-numeric values: {!r} {}".format(
|
||||
value, at_str
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# builtin pytest.approx helper
|
||||
|
||||
|
||||
@@ -39,15 +51,17 @@ class ApproxBase(object):
|
||||
or sequences of numbers.
|
||||
"""
|
||||
|
||||
# Tell numpy to use our `__eq__` operator instead of its
|
||||
# Tell numpy to use our `__eq__` operator instead of its.
|
||||
__array_ufunc__ = None
|
||||
__array_priority__ = 100
|
||||
|
||||
def __init__(self, expected, rel=None, abs=None, nan_ok=False):
|
||||
__tracebackhide__ = True
|
||||
self.expected = expected
|
||||
self.abs = abs
|
||||
self.rel = rel
|
||||
self.nan_ok = nan_ok
|
||||
self._check_type()
|
||||
|
||||
def __repr__(self):
|
||||
raise NotImplementedError
|
||||
@@ -75,20 +89,33 @@ class ApproxBase(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _check_type(self):
|
||||
"""
|
||||
Raise a TypeError if the expected value is not a valid type.
|
||||
"""
|
||||
# This is only a concern if the expected value is a sequence. In every
|
||||
# other case, the approx() function ensures that the expected value has
|
||||
# a numeric type. For this reason, the default is to do nothing. The
|
||||
# classes that deal with sequences should reimplement this method to
|
||||
# raise if there are any non-numeric elements in the sequence.
|
||||
pass
|
||||
|
||||
|
||||
def _recursive_list_map(f, x):
|
||||
if isinstance(x, list):
|
||||
return list(_recursive_list_map(f, xi) for xi in x)
|
||||
else:
|
||||
return f(x)
|
||||
|
||||
|
||||
class ApproxNumpy(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for numpy arrays.
|
||||
Perform approximate comparisons where the expected value is numpy array.
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
# It might be nice to rewrite this function to account for the
|
||||
# shape of the array...
|
||||
import numpy as np
|
||||
|
||||
return "approx({!r})".format(
|
||||
list(self._approx_scalar(x) for x in np.asarray(self.expected))
|
||||
)
|
||||
list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist())
|
||||
return "approx({!r})".format(list_scalars)
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
__cmp__ = _cmp_raises_type_error
|
||||
@@ -126,8 +153,8 @@ class ApproxNumpy(ApproxBase):
|
||||
|
||||
class ApproxMapping(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for mappings where the values are numbers
|
||||
(the keys can be anything).
|
||||
Perform approximate comparisons where the expected value is a mapping with
|
||||
numeric values (the keys can be anything).
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
@@ -145,10 +172,20 @@ class ApproxMapping(ApproxBase):
|
||||
for k in self.expected.keys():
|
||||
yield actual[k], self.expected[k]
|
||||
|
||||
def _check_type(self):
|
||||
__tracebackhide__ = True
|
||||
for key, value in self.expected.items():
|
||||
if isinstance(value, type(self.expected)):
|
||||
msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}"
|
||||
raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
|
||||
elif not isinstance(value, Number):
|
||||
raise _non_numeric_type_error(self.expected, at="key={!r}".format(key))
|
||||
|
||||
|
||||
class ApproxSequence(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for sequences of numbers.
|
||||
Perform approximate comparisons where the expected value is a sequence of
|
||||
numbers.
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
@@ -167,11 +204,23 @@ class ApproxSequence(ApproxBase):
|
||||
def _yield_comparisons(self, actual):
|
||||
return zip(actual, self.expected)
|
||||
|
||||
def _check_type(self):
|
||||
__tracebackhide__ = True
|
||||
for index, x in enumerate(self.expected):
|
||||
if isinstance(x, type(self.expected)):
|
||||
msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}"
|
||||
raise TypeError(msg.format(x, index, pprint.pformat(self.expected)))
|
||||
elif not isinstance(x, Number):
|
||||
raise _non_numeric_type_error(
|
||||
self.expected, at="index {}".format(index)
|
||||
)
|
||||
|
||||
|
||||
class ApproxScalar(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for single numbers only.
|
||||
Perform approximate comparisons where the expected value is a single number.
|
||||
"""
|
||||
|
||||
DEFAULT_ABSOLUTE_TOLERANCE = 1e-12
|
||||
DEFAULT_RELATIVE_TOLERANCE = 1e-6
|
||||
|
||||
@@ -208,7 +257,9 @@ class ApproxScalar(ApproxBase):
|
||||
the pre-specified tolerance.
|
||||
"""
|
||||
if _is_numpy_array(actual):
|
||||
return ApproxNumpy(actual, self.abs, self.rel, self.nan_ok) == self.expected
|
||||
# Call ``__eq__()`` manually to prevent infinite-recursion with
|
||||
# numpy<1.13. See #3748.
|
||||
return all(self.__eq__(a) for a in actual.flat)
|
||||
|
||||
# Short-circuit exact equality.
|
||||
if actual == self.expected:
|
||||
@@ -269,9 +320,7 @@ class ApproxScalar(ApproxBase):
|
||||
# we aren't even going to use it.
|
||||
relative_tolerance = set_default(
|
||||
self.rel, self.DEFAULT_RELATIVE_TOLERANCE
|
||||
) * abs(
|
||||
self.expected
|
||||
)
|
||||
) * abs(self.expected)
|
||||
|
||||
if relative_tolerance < 0:
|
||||
raise ValueError(
|
||||
@@ -285,7 +334,9 @@ class ApproxScalar(ApproxBase):
|
||||
|
||||
|
||||
class ApproxDecimal(ApproxScalar):
|
||||
from decimal import Decimal
|
||||
"""
|
||||
Perform approximate comparisons where the expected value is a decimal.
|
||||
"""
|
||||
|
||||
DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
|
||||
DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
|
||||
@@ -444,32 +495,35 @@ def approx(expected, rel=None, abs=None, nan_ok=False):
|
||||
__ https://docs.python.org/3/reference/datamodel.html#object.__ge__
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
# Delegate the comparison to a class that knows how to deal with the type
|
||||
# of the expected value (e.g. int, float, list, dict, numpy.array, etc).
|
||||
#
|
||||
# This architecture is really driven by the need to support numpy arrays.
|
||||
# The only way to override `==` for arrays without requiring that approx be
|
||||
# the left operand is to inherit the approx object from `numpy.ndarray`.
|
||||
# But that can't be a general solution, because it requires (1) numpy to be
|
||||
# installed and (2) the expected value to be a numpy array. So the general
|
||||
# solution is to delegate each type of expected value to a different class.
|
||||
# The primary responsibility of these classes is to implement ``__eq__()``
|
||||
# and ``__repr__()``. The former is used to actually check if some
|
||||
# "actual" value is equivalent to the given expected value within the
|
||||
# allowed tolerance. The latter is used to show the user the expected
|
||||
# value and tolerance, in the case that a test failed.
|
||||
#
|
||||
# This has the advantage that it made it easy to support mapping types
|
||||
# (i.e. dict). The old code accepted mapping types, but would only compare
|
||||
# their keys, which is probably not what most people would expect.
|
||||
# The actual logic for making approximate comparisons can be found in
|
||||
# ApproxScalar, which is used to compare individual numbers. All of the
|
||||
# other Approx classes eventually delegate to this class. The ApproxBase
|
||||
# class provides some convenient methods and overloads, but isn't really
|
||||
# essential.
|
||||
|
||||
if _is_numpy_array(expected):
|
||||
cls = ApproxNumpy
|
||||
__tracebackhide__ = True
|
||||
|
||||
if isinstance(expected, Decimal):
|
||||
cls = ApproxDecimal
|
||||
elif isinstance(expected, Number):
|
||||
cls = ApproxScalar
|
||||
elif isinstance(expected, Mapping):
|
||||
cls = ApproxMapping
|
||||
elif isinstance(expected, Sequence) and not isinstance(expected, STRING_TYPES):
|
||||
cls = ApproxSequence
|
||||
elif isinstance(expected, Decimal):
|
||||
cls = ApproxDecimal
|
||||
elif _is_numpy_array(expected):
|
||||
cls = ApproxNumpy
|
||||
else:
|
||||
cls = ApproxScalar
|
||||
raise _non_numeric_type_error(expected, at=None)
|
||||
|
||||
return cls(expected, rel, abs, nan_ok)
|
||||
|
||||
@@ -479,17 +533,11 @@ def _is_numpy_array(obj):
|
||||
Return true if the given object is a numpy array. Make a special effort to
|
||||
avoid importing numpy unless it's really necessary.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
for cls in inspect.getmro(type(obj)):
|
||||
if cls.__module__ == "numpy":
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
return isinstance(obj, np.ndarray)
|
||||
except ImportError:
|
||||
pass
|
||||
import sys
|
||||
|
||||
np = sys.modules.get("numpy")
|
||||
if np is not None:
|
||||
return isinstance(obj, np.ndarray)
|
||||
return False
|
||||
|
||||
|
||||
@@ -650,7 +698,6 @@ raises.Exception = fail.Exception
|
||||
|
||||
|
||||
class RaisesContext(object):
|
||||
|
||||
def __init__(self, expected_exception, message, match_expr):
|
||||
self.expected_exception = expected_exception
|
||||
self.message = message
|
||||
|
||||
@@ -193,13 +193,10 @@ class WarningsRecorder(warnings.catch_warnings):
|
||||
|
||||
|
||||
class WarningsChecker(WarningsRecorder):
|
||||
|
||||
def __init__(self, expected_warning=None, match_expr=None):
|
||||
super(WarningsChecker, self).__init__()
|
||||
|
||||
msg = (
|
||||
"exceptions must be old-style classes or " "derived from Warning, not %s"
|
||||
)
|
||||
msg = "exceptions must be old-style classes or " "derived from Warning, not %s"
|
||||
if isinstance(expected_warning, tuple):
|
||||
for exc in expected_warning:
|
||||
if not inspect.isclass(exc):
|
||||
|
||||
196
src/_pytest/reports.py
Normal file
196
src/_pytest/reports.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import py
|
||||
from _pytest._code.code import TerminalRepr
|
||||
|
||||
|
||||
def getslaveinfoline(node):
|
||||
try:
|
||||
return node._slaveinfocache
|
||||
except AttributeError:
|
||||
d = node.slaveinfo
|
||||
ver = "%s.%s.%s" % d["version_info"][:3]
|
||||
node._slaveinfocache = s = "[%s] %s -- Python %s %s" % (
|
||||
d["id"],
|
||||
d["sysplatform"],
|
||||
ver,
|
||||
d["executable"],
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
class BaseReport(object):
|
||||
def __init__(self, **kw):
|
||||
self.__dict__.update(kw)
|
||||
|
||||
def toterminal(self, out):
|
||||
if hasattr(self, "node"):
|
||||
out.line(getslaveinfoline(self.node))
|
||||
|
||||
longrepr = self.longrepr
|
||||
if longrepr is None:
|
||||
return
|
||||
|
||||
if hasattr(longrepr, "toterminal"):
|
||||
longrepr.toterminal(out)
|
||||
else:
|
||||
try:
|
||||
out.line(longrepr)
|
||||
except UnicodeEncodeError:
|
||||
out.line("<unprintable longrepr>")
|
||||
|
||||
def get_sections(self, prefix):
|
||||
for name, content in self.sections:
|
||||
if name.startswith(prefix):
|
||||
yield prefix, content
|
||||
|
||||
@property
|
||||
def longreprtext(self):
|
||||
"""
|
||||
Read-only property that returns the full string representation
|
||||
of ``longrepr``.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
tw = py.io.TerminalWriter(stringio=True)
|
||||
tw.hasmarkup = False
|
||||
self.toterminal(tw)
|
||||
exc = tw.stringio.getvalue()
|
||||
return exc.strip()
|
||||
|
||||
@property
|
||||
def caplog(self):
|
||||
"""Return captured log lines, if log capturing is enabled
|
||||
|
||||
.. versionadded:: 3.5
|
||||
"""
|
||||
return "\n".join(
|
||||
content for (prefix, content) in self.get_sections("Captured log")
|
||||
)
|
||||
|
||||
@property
|
||||
def capstdout(self):
|
||||
"""Return captured text from stdout, if capturing is enabled
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return "".join(
|
||||
content for (prefix, content) in self.get_sections("Captured stdout")
|
||||
)
|
||||
|
||||
@property
|
||||
def capstderr(self):
|
||||
"""Return captured text from stderr, if capturing is enabled
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return "".join(
|
||||
content for (prefix, content) in self.get_sections("Captured stderr")
|
||||
)
|
||||
|
||||
passed = property(lambda x: x.outcome == "passed")
|
||||
failed = property(lambda x: x.outcome == "failed")
|
||||
skipped = property(lambda x: x.outcome == "skipped")
|
||||
|
||||
@property
|
||||
def fspath(self):
|
||||
return self.nodeid.split("::")[0]
|
||||
|
||||
|
||||
class TestReport(BaseReport):
|
||||
""" Basic test report object (also used for setup and teardown calls if
|
||||
they fail).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nodeid,
|
||||
location,
|
||||
keywords,
|
||||
outcome,
|
||||
longrepr,
|
||||
when,
|
||||
sections=(),
|
||||
duration=0,
|
||||
user_properties=(),
|
||||
**extra
|
||||
):
|
||||
#: normalized collection node id
|
||||
self.nodeid = nodeid
|
||||
|
||||
#: a (filesystempath, lineno, domaininfo) tuple indicating the
|
||||
#: actual location of a test item - it might be different from the
|
||||
#: collected one e.g. if a method is inherited from a different module.
|
||||
self.location = location
|
||||
|
||||
#: a name -> value dictionary containing all keywords and
|
||||
#: markers associated with a test invocation.
|
||||
self.keywords = keywords
|
||||
|
||||
#: test outcome, always one of "passed", "failed", "skipped".
|
||||
self.outcome = outcome
|
||||
|
||||
#: None or a failure representation.
|
||||
self.longrepr = longrepr
|
||||
|
||||
#: one of 'setup', 'call', 'teardown' to indicate runtest phase.
|
||||
self.when = when
|
||||
|
||||
#: user properties is a list of tuples (name, value) that holds user
|
||||
#: defined properties of the test
|
||||
self.user_properties = user_properties
|
||||
|
||||
#: list of pairs ``(str, str)`` of extra information which needs to
|
||||
#: marshallable. Used by pytest to add captured text
|
||||
#: from ``stdout`` and ``stderr``, but may be used by other plugins
|
||||
#: to add arbitrary information to reports.
|
||||
self.sections = list(sections)
|
||||
|
||||
#: time it took to run just the test
|
||||
self.duration = duration
|
||||
|
||||
self.__dict__.update(extra)
|
||||
|
||||
def __repr__(self):
|
||||
return "<TestReport %r when=%r outcome=%r>" % (
|
||||
self.nodeid,
|
||||
self.when,
|
||||
self.outcome,
|
||||
)
|
||||
|
||||
|
||||
class TeardownErrorReport(BaseReport):
|
||||
outcome = "failed"
|
||||
when = "teardown"
|
||||
|
||||
def __init__(self, longrepr, **extra):
|
||||
self.longrepr = longrepr
|
||||
self.sections = []
|
||||
self.__dict__.update(extra)
|
||||
|
||||
|
||||
class CollectReport(BaseReport):
|
||||
def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
|
||||
self.nodeid = nodeid
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
self.result = result or []
|
||||
self.sections = list(sections)
|
||||
self.__dict__.update(extra)
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return (self.fspath, None, self.fspath)
|
||||
|
||||
def __repr__(self):
|
||||
return "<CollectReport %r lenresult=%s outcome=%r>" % (
|
||||
self.nodeid,
|
||||
len(self.result),
|
||||
self.outcome,
|
||||
)
|
||||
|
||||
|
||||
class CollectErrorRepr(TerminalRepr):
|
||||
def __init__(self, msg):
|
||||
self.longrepr = msg
|
||||
|
||||
def toterminal(self, out):
|
||||
out.line(self.longrepr, red=True)
|
||||
@@ -68,7 +68,6 @@ def generic_path(item):
|
||||
|
||||
|
||||
class ResultLog(object):
|
||||
|
||||
def __init__(self, config, logfile):
|
||||
self.config = config
|
||||
self.logfile = logfile # preferably line buffered
|
||||
|
||||
@@ -7,9 +7,11 @@ import sys
|
||||
from time import time
|
||||
|
||||
import py
|
||||
from _pytest._code.code import TerminalRepr, ExceptionInfo
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest.outcomes import skip, Skipped, TEST_OUTCOME
|
||||
|
||||
from .reports import TestReport, CollectReport, CollectErrorRepr
|
||||
|
||||
#
|
||||
# pytest plugin hooks
|
||||
|
||||
@@ -186,6 +188,7 @@ def call_runtest_hook(item, when, **kwds):
|
||||
|
||||
class CallInfo(object):
|
||||
""" Result/Exception info a function invocation. """
|
||||
|
||||
#: None or ExceptionInfo object.
|
||||
excinfo = None
|
||||
|
||||
@@ -214,97 +217,6 @@ class CallInfo(object):
|
||||
return "<CallInfo when=%r %s>" % (self.when, status)
|
||||
|
||||
|
||||
def getslaveinfoline(node):
|
||||
try:
|
||||
return node._slaveinfocache
|
||||
except AttributeError:
|
||||
d = node.slaveinfo
|
||||
ver = "%s.%s.%s" % d["version_info"][:3]
|
||||
node._slaveinfocache = s = "[%s] %s -- Python %s %s" % (
|
||||
d["id"], d["sysplatform"], ver, d["executable"]
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
class BaseReport(object):
|
||||
|
||||
def __init__(self, **kw):
|
||||
self.__dict__.update(kw)
|
||||
|
||||
def toterminal(self, out):
|
||||
if hasattr(self, "node"):
|
||||
out.line(getslaveinfoline(self.node))
|
||||
|
||||
longrepr = self.longrepr
|
||||
if longrepr is None:
|
||||
return
|
||||
|
||||
if hasattr(longrepr, "toterminal"):
|
||||
longrepr.toterminal(out)
|
||||
else:
|
||||
try:
|
||||
out.line(longrepr)
|
||||
except UnicodeEncodeError:
|
||||
out.line("<unprintable longrepr>")
|
||||
|
||||
def get_sections(self, prefix):
|
||||
for name, content in self.sections:
|
||||
if name.startswith(prefix):
|
||||
yield prefix, content
|
||||
|
||||
@property
|
||||
def longreprtext(self):
|
||||
"""
|
||||
Read-only property that returns the full string representation
|
||||
of ``longrepr``.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
tw = py.io.TerminalWriter(stringio=True)
|
||||
tw.hasmarkup = False
|
||||
self.toterminal(tw)
|
||||
exc = tw.stringio.getvalue()
|
||||
return exc.strip()
|
||||
|
||||
@property
|
||||
def caplog(self):
|
||||
"""Return captured log lines, if log capturing is enabled
|
||||
|
||||
.. versionadded:: 3.5
|
||||
"""
|
||||
return "\n".join(
|
||||
content for (prefix, content) in self.get_sections("Captured log")
|
||||
)
|
||||
|
||||
@property
|
||||
def capstdout(self):
|
||||
"""Return captured text from stdout, if capturing is enabled
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return "".join(
|
||||
content for (prefix, content) in self.get_sections("Captured stdout")
|
||||
)
|
||||
|
||||
@property
|
||||
def capstderr(self):
|
||||
"""Return captured text from stderr, if capturing is enabled
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return "".join(
|
||||
content for (prefix, content) in self.get_sections("Captured stderr")
|
||||
)
|
||||
|
||||
passed = property(lambda x: x.outcome == "passed")
|
||||
failed = property(lambda x: x.outcome == "failed")
|
||||
skipped = property(lambda x: x.outcome == "skipped")
|
||||
|
||||
@property
|
||||
def fspath(self):
|
||||
return self.nodeid.split("::")[0]
|
||||
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
when = call.when
|
||||
duration = call.stop - call.start
|
||||
@@ -345,76 +257,6 @@ def pytest_runtest_makereport(item, call):
|
||||
)
|
||||
|
||||
|
||||
class TestReport(BaseReport):
|
||||
""" Basic test report object (also used for setup and teardown calls if
|
||||
they fail).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nodeid,
|
||||
location,
|
||||
keywords,
|
||||
outcome,
|
||||
longrepr,
|
||||
when,
|
||||
sections=(),
|
||||
duration=0,
|
||||
user_properties=(),
|
||||
**extra
|
||||
):
|
||||
#: normalized collection node id
|
||||
self.nodeid = nodeid
|
||||
|
||||
#: a (filesystempath, lineno, domaininfo) tuple indicating the
|
||||
#: actual location of a test item - it might be different from the
|
||||
#: collected one e.g. if a method is inherited from a different module.
|
||||
self.location = location
|
||||
|
||||
#: a name -> value dictionary containing all keywords and
|
||||
#: markers associated with a test invocation.
|
||||
self.keywords = keywords
|
||||
|
||||
#: test outcome, always one of "passed", "failed", "skipped".
|
||||
self.outcome = outcome
|
||||
|
||||
#: None or a failure representation.
|
||||
self.longrepr = longrepr
|
||||
|
||||
#: one of 'setup', 'call', 'teardown' to indicate runtest phase.
|
||||
self.when = when
|
||||
|
||||
#: user properties is a list of tuples (name, value) that holds user
|
||||
#: defined properties of the test
|
||||
self.user_properties = user_properties
|
||||
|
||||
#: list of pairs ``(str, str)`` of extra information which needs to
|
||||
#: marshallable. Used by pytest to add captured text
|
||||
#: from ``stdout`` and ``stderr``, but may be used by other plugins
|
||||
#: to add arbitrary information to reports.
|
||||
self.sections = list(sections)
|
||||
|
||||
#: time it took to run just the test
|
||||
self.duration = duration
|
||||
|
||||
self.__dict__.update(extra)
|
||||
|
||||
def __repr__(self):
|
||||
return "<TestReport %r when=%r outcome=%r>" % (
|
||||
self.nodeid, self.when, self.outcome
|
||||
)
|
||||
|
||||
|
||||
class TeardownErrorReport(BaseReport):
|
||||
outcome = "failed"
|
||||
when = "teardown"
|
||||
|
||||
def __init__(self, longrepr, **extra):
|
||||
self.longrepr = longrepr
|
||||
self.sections = []
|
||||
self.__dict__.update(extra)
|
||||
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
call = CallInfo(lambda: list(collector.collect()), "collect")
|
||||
longrepr = None
|
||||
@@ -441,35 +283,6 @@ def pytest_make_collect_report(collector):
|
||||
return rep
|
||||
|
||||
|
||||
class CollectReport(BaseReport):
|
||||
|
||||
def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
|
||||
self.nodeid = nodeid
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
self.result = result or []
|
||||
self.sections = list(sections)
|
||||
self.__dict__.update(extra)
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return (self.fspath, None, self.fspath)
|
||||
|
||||
def __repr__(self):
|
||||
return "<CollectReport %r lenresult=%s outcome=%r>" % (
|
||||
self.nodeid, len(self.result), self.outcome
|
||||
)
|
||||
|
||||
|
||||
class CollectErrorRepr(TerminalRepr):
|
||||
|
||||
def __init__(self, msg):
|
||||
self.longrepr = msg
|
||||
|
||||
def toterminal(self, out):
|
||||
out.line(self.longrepr, red=True)
|
||||
|
||||
|
||||
class SetupState(object):
|
||||
""" shared state for setting up/tearing down test items or collectors. """
|
||||
|
||||
@@ -529,7 +342,7 @@ class SetupState(object):
|
||||
def _teardown_towards(self, needed_collectors):
|
||||
exc = None
|
||||
while self.stack:
|
||||
if self.stack == needed_collectors[:len(self.stack)]:
|
||||
if self.stack == needed_collectors[: len(self.stack)]:
|
||||
break
|
||||
try:
|
||||
self._pop_and_teardown()
|
||||
@@ -551,7 +364,7 @@ class SetupState(object):
|
||||
for col in self.stack:
|
||||
if hasattr(col, "_prepare_exc"):
|
||||
py.builtin._reraise(*col._prepare_exc)
|
||||
for col in needed_collectors[len(self.stack):]:
|
||||
for col in needed_collectors[len(self.stack) :]:
|
||||
self.stack.append(col)
|
||||
try:
|
||||
col.setup()
|
||||
|
||||
@@ -157,9 +157,11 @@ def pytest_runtest_makereport(item, call):
|
||||
else:
|
||||
rep.outcome = "passed"
|
||||
rep.wasxfail = explanation
|
||||
elif getattr(item, "_skipped_by_mark", False) and rep.skipped and type(
|
||||
rep.longrepr
|
||||
) is tuple:
|
||||
elif (
|
||||
getattr(item, "_skipped_by_mark", False)
|
||||
and rep.skipped
|
||||
and type(rep.longrepr) is tuple
|
||||
):
|
||||
# skipped by mark.skipif; change the location of the failure
|
||||
# to point to the item definition, otherwise it will display
|
||||
# the location of where the skip exception was raised within pytest
|
||||
@@ -274,7 +276,6 @@ def show_skipped(terminalreporter, lines):
|
||||
|
||||
|
||||
def shower(stat, format):
|
||||
|
||||
def show_(terminalreporter, lines):
|
||||
return show_simple(terminalreporter, lines, stat, format)
|
||||
|
||||
|
||||
@@ -220,7 +220,6 @@ class WarningReport(object):
|
||||
|
||||
|
||||
class TerminalReporter(object):
|
||||
|
||||
def __init__(self, config, file=None):
|
||||
import _pytest.config
|
||||
|
||||
@@ -407,13 +406,16 @@ class TerminalReporter(object):
|
||||
def pytest_runtest_logfinish(self, nodeid):
|
||||
if self.verbosity <= 0 and self._show_progress_info:
|
||||
self._progress_nodeids_reported.add(nodeid)
|
||||
last_item = len(
|
||||
self._progress_nodeids_reported
|
||||
) == self._session.testscollected
|
||||
last_item = (
|
||||
len(self._progress_nodeids_reported) == self._session.testscollected
|
||||
)
|
||||
if last_item:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
past_edge = self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width
|
||||
past_edge = (
|
||||
self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1
|
||||
>= self._screen_width
|
||||
)
|
||||
if past_edge:
|
||||
msg = self._get_progress_information_message()
|
||||
self._tw.write(msg + "\n", cyan=True)
|
||||
@@ -462,8 +464,8 @@ class TerminalReporter(object):
|
||||
line = "collected "
|
||||
else:
|
||||
line = "collecting "
|
||||
line += str(self._numcollected) + " item" + (
|
||||
"" if self._numcollected == 1 else "s"
|
||||
line += (
|
||||
str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
|
||||
)
|
||||
if errors:
|
||||
line += " / %d errors" % errors
|
||||
@@ -495,7 +497,9 @@ class TerminalReporter(object):
|
||||
verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
|
||||
msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3])
|
||||
msg += ", pytest-%s, py-%s, pluggy-%s" % (
|
||||
pytest.__version__, py.__version__, pluggy.__version__
|
||||
pytest.__version__,
|
||||
py.__version__,
|
||||
pluggy.__version__,
|
||||
)
|
||||
if (
|
||||
self.verbosity > 0
|
||||
@@ -563,10 +567,10 @@ class TerminalReporter(object):
|
||||
for item in items:
|
||||
needed_collectors = item.listchain()[1:] # strip root node
|
||||
while stack:
|
||||
if stack == needed_collectors[:len(stack)]:
|
||||
if stack == needed_collectors[: len(stack)]:
|
||||
break
|
||||
stack.pop()
|
||||
for col in needed_collectors[len(stack):]:
|
||||
for col in needed_collectors[len(stack) :]:
|
||||
stack.append(col)
|
||||
# if col.name == "()":
|
||||
# continue
|
||||
@@ -624,11 +628,10 @@ class TerminalReporter(object):
|
||||
)
|
||||
|
||||
def _locationline(self, nodeid, fspath, lineno, domain):
|
||||
|
||||
def mkrel(nodeid):
|
||||
line = self.config.cwd_relative_nodeid(nodeid)
|
||||
if domain and line.endswith(domain):
|
||||
line = line[:-len(domain)]
|
||||
line = line[: -len(domain)]
|
||||
values = domain.split("[")
|
||||
values[0] = values[0].replace(".", "::") # don't replace '.' in params
|
||||
line += "[".join(values)
|
||||
|
||||
@@ -69,6 +69,7 @@ class UnitTestCase(Class):
|
||||
class TestCaseFunction(Function):
|
||||
nofuncargs = True
|
||||
_excinfo = None
|
||||
_testcase = None
|
||||
|
||||
def setup(self):
|
||||
self._testcase = self.parent.obj(self.name)
|
||||
@@ -163,15 +164,13 @@ class TestCaseFunction(Function):
|
||||
# implements the skipping machinery (see #2137)
|
||||
# analog to pythons Lib/unittest/case.py:run
|
||||
testMethod = getattr(self._testcase, self._testcase._testMethodName)
|
||||
if (
|
||||
getattr(self._testcase.__class__, "__unittest_skip__", False)
|
||||
or getattr(testMethod, "__unittest_skip__", False)
|
||||
if getattr(self._testcase.__class__, "__unittest_skip__", False) or getattr(
|
||||
testMethod, "__unittest_skip__", False
|
||||
):
|
||||
# If the class or method was skipped.
|
||||
skip_why = (
|
||||
getattr(self._testcase.__class__, "__unittest_skip_why__", "")
|
||||
or getattr(testMethod, "__unittest_skip_why__", "")
|
||||
)
|
||||
skip_why = getattr(
|
||||
self._testcase.__class__, "__unittest_skip_why__", ""
|
||||
) or getattr(testMethod, "__unittest_skip_why__", "")
|
||||
try: # PY3, unittest2 on PY2
|
||||
self._testcase._addSkip(self, self._testcase, skip_why)
|
||||
except TypeError: # PY2
|
||||
|
||||
@@ -49,6 +49,14 @@ def pytest_addoption(parser):
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"filterwarnings(warning): add a warning filter to the given test. "
|
||||
"see http://pytest.org/latest/warnings.html#pytest-mark-filterwarnings ",
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def catch_warnings_for_item(item):
|
||||
"""
|
||||
@@ -75,9 +83,8 @@ def catch_warnings_for_item(item):
|
||||
warn_msg = warning.message
|
||||
unicode_warning = False
|
||||
|
||||
if (
|
||||
compat._PY2
|
||||
and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args)
|
||||
if compat._PY2 and any(
|
||||
isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args
|
||||
):
|
||||
new_args = []
|
||||
for m in warn_msg.args:
|
||||
|
||||
@@ -18,7 +18,7 @@ from _pytest.mark import MARK_GEN as mark, param
|
||||
from _pytest.main import Session
|
||||
from _pytest.nodes import Item, Collector, File
|
||||
from _pytest.fixtures import fillfixtures as _fillfuncargs
|
||||
from _pytest.python import Module, Class, Instance, Function, Generator
|
||||
from _pytest.python import Package, Module, Class, Instance, Function, Generator
|
||||
|
||||
from _pytest.python_api import approx, raises
|
||||
|
||||
@@ -50,6 +50,7 @@ __all__ = [
|
||||
"Item",
|
||||
"File",
|
||||
"Collector",
|
||||
"Package",
|
||||
"Session",
|
||||
"Module",
|
||||
"Class",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
"""
|
||||
Invoke tasks to help with pytest development and release process.
|
||||
"""
|
||||
|
||||
import invoke
|
||||
|
||||
from . import generate
|
||||
|
||||
|
||||
ns = invoke.Collection(generate)
|
||||
@@ -1,118 +0,0 @@
|
||||
"""
|
||||
Invoke development tasks.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from subprocess import check_output, check_call
|
||||
|
||||
import invoke
|
||||
|
||||
|
||||
@invoke.task(help={"version": "version being released"})
|
||||
def announce(ctx, version):
|
||||
"""Generates a new release announcement entry in the docs."""
|
||||
# Get our list of authors
|
||||
stdout = check_output(["git", "describe", "--abbrev=0", "--tags"])
|
||||
stdout = stdout.decode("utf-8")
|
||||
last_version = stdout.strip()
|
||||
|
||||
stdout = check_output(
|
||||
["git", "log", "{}..HEAD".format(last_version), "--format=%aN"]
|
||||
)
|
||||
stdout = stdout.decode("utf-8")
|
||||
|
||||
contributors = set(stdout.splitlines())
|
||||
|
||||
template_name = "release.minor.rst" if version.endswith(
|
||||
".0"
|
||||
) else "release.patch.rst"
|
||||
template_text = Path(__file__).parent.joinpath(template_name).read_text(
|
||||
encoding="UTF-8"
|
||||
)
|
||||
|
||||
contributors_text = "\n".join(
|
||||
"* {}".format(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.write_text(text, encoding="UTF-8")
|
||||
print("[generate.announce] Generated {}".format(target.name))
|
||||
|
||||
# Update index with the new release entry
|
||||
index_path = Path(__file__).parent.joinpath("../doc/en/announce/index.rst")
|
||||
lines = index_path.read_text(encoding="UTF-8").splitlines()
|
||||
indent = " "
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith("{}release-".format(indent)):
|
||||
new_line = indent + target.stem
|
||||
if line != new_line:
|
||||
lines.insert(index, new_line)
|
||||
index_path.write_text("\n".join(lines) + "\n", encoding="UTF-8")
|
||||
print("[generate.announce] Updated {}".format(index_path.name))
|
||||
else:
|
||||
print(
|
||||
"[generate.announce] Skip {} (already contains release)".format(
|
||||
index_path.name
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
check_call(["git", "add", str(target)])
|
||||
|
||||
|
||||
@invoke.task()
|
||||
def regen(ctx):
|
||||
"""Call regendoc tool to update examples and pytest output in the docs."""
|
||||
print("[generate.regen] Updating docs")
|
||||
check_call(["tox", "-e", "regen"])
|
||||
|
||||
|
||||
@invoke.task()
|
||||
def make_tag(ctx, version):
|
||||
"""Create a new, local tag for the release, only if the repository is clean."""
|
||||
from git import Repo
|
||||
|
||||
repo = Repo(".")
|
||||
if repo.is_dirty():
|
||||
print("Current repository is dirty. Please commit any changes and try again.")
|
||||
raise invoke.Exit(code=2)
|
||||
|
||||
tag_names = [x.name for x in repo.tags]
|
||||
if version in tag_names:
|
||||
print("[generate.make_tag] Delete existing tag {}".format(version))
|
||||
repo.delete_tag(version)
|
||||
|
||||
print("[generate.make_tag] Create tag {}".format(version))
|
||||
repo.create_tag(version)
|
||||
|
||||
|
||||
@invoke.task(help={"version": "version being released"})
|
||||
def pre_release(ctx, version):
|
||||
"""Generates new docs, release announcements and creates a local tag."""
|
||||
announce(ctx, version)
|
||||
regen(ctx)
|
||||
changelog(ctx, version, write_out=True)
|
||||
|
||||
msg = "Preparing release version {}".format(version)
|
||||
check_call(["git", "commit", "-a", "-m", msg])
|
||||
|
||||
make_tag(ctx, version)
|
||||
|
||||
print()
|
||||
print("[generate.pre_release] Please push your branch and open a PR.")
|
||||
|
||||
|
||||
@invoke.task(
|
||||
help={
|
||||
"version": "version being released",
|
||||
"write_out": "write changes to the actual changelog",
|
||||
}
|
||||
)
|
||||
def changelog(ctx, version, write_out=False):
|
||||
if write_out:
|
||||
addopts = []
|
||||
else:
|
||||
addopts = ["--draft"]
|
||||
check_call(["towncrier", "--yes", "--version", version] + addopts)
|
||||
@@ -1,6 +0,0 @@
|
||||
-e .
|
||||
gitpython
|
||||
invoke
|
||||
towncrier
|
||||
tox
|
||||
wheel
|
||||
@@ -13,15 +13,8 @@ from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR
|
||||
|
||||
|
||||
class TestGeneralUsage(object):
|
||||
|
||||
def test_config_error(self, testdir):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
def pytest_configure(config):
|
||||
import pytest
|
||||
raise pytest.UsageError("hello")
|
||||
"""
|
||||
)
|
||||
testdir.copy_example("conftest_usageerror/conftest.py")
|
||||
result = testdir.runpytest(testdir.tmpdir)
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines(["*ERROR: hello"])
|
||||
@@ -171,18 +164,7 @@ class TestGeneralUsage(object):
|
||||
result.stdout.fnmatch_lines(["*1 skip*"])
|
||||
|
||||
def test_issue88_initial_file_multinodes(self, testdir):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
import pytest
|
||||
class MyFile(pytest.File):
|
||||
def collect(self):
|
||||
return [MyItem("hello", parent=self)]
|
||||
def pytest_collect_file(path, parent):
|
||||
return MyFile(path, parent)
|
||||
class MyItem(pytest.Item):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
testdir.copy_example("issue88_initial_file_multinodes")
|
||||
p = testdir.makepyfile("def test_hello(): pass")
|
||||
result = testdir.runpytest(p, "--collect-only")
|
||||
result.stdout.fnmatch_lines(["*MyFile*test_issue88*", "*Module*test_issue88*"])
|
||||
@@ -458,7 +440,6 @@ class TestGeneralUsage(object):
|
||||
|
||||
|
||||
class TestInvocationVariants(object):
|
||||
|
||||
def test_earlyinit(self, testdir):
|
||||
p = testdir.makepyfile(
|
||||
"""
|
||||
@@ -557,9 +538,7 @@ class TestInvocationVariants(object):
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
def test_invoke_plugin_api(self, testdir, capsys):
|
||||
|
||||
class MyPlugin(object):
|
||||
|
||||
def pytest_addoption(self, parser):
|
||||
parser.addoption("--myopt")
|
||||
|
||||
@@ -798,9 +777,10 @@ class TestInvocationVariants(object):
|
||||
"""Test backward compatibility for get_plugin_manager function. See #787."""
|
||||
import _pytest.config
|
||||
|
||||
assert type(
|
||||
_pytest.config.get_plugin_manager()
|
||||
) is _pytest.config.PytestPluginManager
|
||||
assert (
|
||||
type(_pytest.config.get_plugin_manager())
|
||||
is _pytest.config.PytestPluginManager
|
||||
)
|
||||
|
||||
def test_has_plugin(self, request):
|
||||
"""Test hasplugin function of the plugin manager (#932)."""
|
||||
@@ -1064,3 +1044,10 @@ def test_frame_leak_on_failing_test(testdir):
|
||||
)
|
||||
result = testdir.runpytest_subprocess()
|
||||
result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"])
|
||||
|
||||
|
||||
def test_fixture_mock_integration(testdir):
|
||||
"""Test that decorators applied to fixture are left working (#3774)"""
|
||||
p = testdir.copy_example("acceptance/fixture_mock_integration.py")
|
||||
result = testdir.runpytest(p)
|
||||
result.stdout.fnmatch_lines("*1 passed*")
|
||||
|
||||
@@ -26,7 +26,6 @@ def test_code_gives_back_name_for_not_existing_file():
|
||||
|
||||
|
||||
def test_code_with_class():
|
||||
|
||||
class A(object):
|
||||
pass
|
||||
|
||||
@@ -54,7 +53,6 @@ def test_code_source():
|
||||
|
||||
|
||||
def test_frame_getsourcelineno_myself():
|
||||
|
||||
def func():
|
||||
return sys._getframe(0)
|
||||
|
||||
@@ -65,7 +63,6 @@ def test_frame_getsourcelineno_myself():
|
||||
|
||||
|
||||
def test_getstatement_empty_fullsource():
|
||||
|
||||
def func():
|
||||
return sys._getframe(0)
|
||||
|
||||
@@ -111,7 +108,6 @@ def test_unicode_handling_syntax_error():
|
||||
|
||||
|
||||
def test_code_getargs():
|
||||
|
||||
def f1(x):
|
||||
pass
|
||||
|
||||
@@ -138,7 +134,6 @@ def test_code_getargs():
|
||||
|
||||
|
||||
def test_frame_getargs():
|
||||
|
||||
def f1(x):
|
||||
return sys._getframe(0)
|
||||
|
||||
@@ -165,7 +160,6 @@ def test_frame_getargs():
|
||||
|
||||
|
||||
class TestExceptionInfo(object):
|
||||
|
||||
def test_bad_getsource(self):
|
||||
try:
|
||||
if False:
|
||||
@@ -178,7 +172,6 @@ class TestExceptionInfo(object):
|
||||
|
||||
|
||||
class TestTracebackEntry(object):
|
||||
|
||||
def test_getsource(self):
|
||||
try:
|
||||
if False:
|
||||
@@ -194,7 +187,6 @@ class TestTracebackEntry(object):
|
||||
|
||||
|
||||
class TestReprFuncArgs(object):
|
||||
|
||||
def test_not_raise_exception_with_mixed_encoding(self):
|
||||
from _pytest._code.code import ReprFuncArgs
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function
|
||||
import operator
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
import _pytest
|
||||
import py
|
||||
import pytest
|
||||
@@ -65,7 +66,6 @@ def test_excinfo_simple():
|
||||
|
||||
|
||||
def test_excinfo_getstatement():
|
||||
|
||||
def g():
|
||||
raise ValueError
|
||||
|
||||
@@ -112,7 +112,6 @@ def h():
|
||||
|
||||
|
||||
class TestTraceback_f_g_h(object):
|
||||
|
||||
def setup_method(self, method):
|
||||
try:
|
||||
h()
|
||||
@@ -194,7 +193,6 @@ class TestTraceback_f_g_h(object):
|
||||
],
|
||||
)
|
||||
def test_traceback_filter_selective(self, tracebackhide, matching):
|
||||
|
||||
def f():
|
||||
#
|
||||
raise ValueError
|
||||
@@ -224,7 +222,6 @@ class TestTraceback_f_g_h(object):
|
||||
assert len(ntraceback) == len(traceback) - 1
|
||||
|
||||
def test_traceback_recursion_index(self):
|
||||
|
||||
def f(n):
|
||||
if n < 10:
|
||||
n += 1
|
||||
@@ -236,7 +233,6 @@ class TestTraceback_f_g_h(object):
|
||||
assert recindex == 3
|
||||
|
||||
def test_traceback_only_specific_recursion_errors(self, monkeypatch):
|
||||
|
||||
def f(n):
|
||||
if n == 0:
|
||||
raise RuntimeError("hello")
|
||||
@@ -248,7 +244,6 @@ class TestTraceback_f_g_h(object):
|
||||
assert "RuntimeError: hello" in str(repr.reprcrash)
|
||||
|
||||
def test_traceback_no_recursion_index(self):
|
||||
|
||||
def do_stuff():
|
||||
raise RuntimeError
|
||||
|
||||
@@ -288,7 +283,6 @@ class TestTraceback_f_g_h(object):
|
||||
assert excinfo.traceback.recursionindex() is None
|
||||
|
||||
def test_traceback_getcrashentry(self):
|
||||
|
||||
def i():
|
||||
__tracebackhide__ = True
|
||||
raise ValueError
|
||||
@@ -312,7 +306,6 @@ class TestTraceback_f_g_h(object):
|
||||
assert entry.frame.code.name == "h"
|
||||
|
||||
def test_traceback_getcrashentry_empty(self):
|
||||
|
||||
def g():
|
||||
__tracebackhide__ = True
|
||||
raise ValueError
|
||||
@@ -429,10 +422,8 @@ def test_match_raises_error(testdir):
|
||||
|
||||
|
||||
class TestFormattedExcinfo(object):
|
||||
|
||||
@pytest.fixture
|
||||
def importasmod(self, request):
|
||||
|
||||
def importasmod(source):
|
||||
source = _pytest._code.Source(source)
|
||||
tmpdir = request.getfixturevalue("tmpdir")
|
||||
@@ -519,7 +510,6 @@ raise ValueError()
|
||||
pr = FormattedExcinfo()
|
||||
|
||||
class FakeCode(object):
|
||||
|
||||
class raw(object):
|
||||
co_filename = "?"
|
||||
|
||||
@@ -537,7 +527,6 @@ raise ValueError()
|
||||
f_globals = {}
|
||||
|
||||
class FakeTracebackEntry(_pytest._code.Traceback.Entry):
|
||||
|
||||
def __init__(self, tb, excinfo=None):
|
||||
self.lineno = 5 + 3
|
||||
|
||||
@@ -591,6 +580,18 @@ raise ValueError()
|
||||
assert reprlocals.lines[2] == "y = 5"
|
||||
assert reprlocals.lines[3] == "z = 7"
|
||||
|
||||
def test_repr_local_truncated(self):
|
||||
loc = {"l": [i for i in range(10)]}
|
||||
p = FormattedExcinfo(showlocals=True)
|
||||
truncated_reprlocals = p.repr_locals(loc)
|
||||
assert truncated_reprlocals.lines
|
||||
assert truncated_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, ...]"
|
||||
|
||||
q = FormattedExcinfo(showlocals=True, truncate_locals=False)
|
||||
full_reprlocals = q.repr_locals(loc)
|
||||
assert full_reprlocals.lines
|
||||
assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
|
||||
|
||||
def test_repr_tracebackentry_lines(self, importasmod):
|
||||
mod = importasmod(
|
||||
"""
|
||||
@@ -882,7 +883,6 @@ raise ValueError()
|
||||
from _pytest._code.code import TerminalRepr
|
||||
|
||||
class MyRepr(TerminalRepr):
|
||||
|
||||
def toterminal(self, tw):
|
||||
tw.line(py.builtin._totext("я", "utf-8"))
|
||||
|
||||
@@ -1266,6 +1266,50 @@ raise ValueError()
|
||||
]
|
||||
)
|
||||
|
||||
@pytest.mark.skipif("sys.version_info[0] < 3")
|
||||
def test_exc_chain_repr_cycle(self, importasmod):
|
||||
mod = importasmod(
|
||||
"""
|
||||
class Err(Exception):
|
||||
pass
|
||||
def fail():
|
||||
return 0 / 0
|
||||
def reraise():
|
||||
try:
|
||||
fail()
|
||||
except ZeroDivisionError as e:
|
||||
raise Err() from e
|
||||
def unreraise():
|
||||
try:
|
||||
reraise()
|
||||
except Err as e:
|
||||
raise e.__cause__
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ZeroDivisionError, mod.unreraise)
|
||||
r = excinfo.getrepr(style="short")
|
||||
tw = TWMock()
|
||||
r.toterminal(tw)
|
||||
out = "\n".join(line for line in tw.lines if isinstance(line, str))
|
||||
expected_out = textwrap.dedent(
|
||||
"""\
|
||||
:13: in unreraise
|
||||
reraise()
|
||||
:10: in reraise
|
||||
raise Err() from e
|
||||
E test_exc_chain_repr_cycle0.mod.Err
|
||||
|
||||
During handling of the above exception, another exception occurred:
|
||||
:15: in unreraise
|
||||
raise e.__cause__
|
||||
:8: in reraise
|
||||
fail()
|
||||
:5: in fail
|
||||
return 0 / 0
|
||||
E ZeroDivisionError: division by zero"""
|
||||
)
|
||||
assert out == expected_out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("style", ["short", "long"])
|
||||
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])
|
||||
@@ -1303,7 +1347,6 @@ def test_exception_repr_extraction_error_on_recursion():
|
||||
"""
|
||||
|
||||
class numpy_like(object):
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) is numpy_like:
|
||||
raise ValueError(
|
||||
@@ -1343,7 +1386,6 @@ def test_no_recursion_index_on_recursion_error():
|
||||
try:
|
||||
|
||||
class RecursionDepthError(object):
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self, "_" + attr)
|
||||
|
||||
|
||||
@@ -58,9 +58,7 @@ def test_source_from_function():
|
||||
|
||||
|
||||
def test_source_from_method():
|
||||
|
||||
class TestClass(object):
|
||||
|
||||
def test_method(self):
|
||||
pass
|
||||
|
||||
@@ -75,7 +73,6 @@ def test_source_from_lines():
|
||||
|
||||
|
||||
def test_source_from_inner_function():
|
||||
|
||||
def f():
|
||||
pass
|
||||
|
||||
@@ -199,7 +196,6 @@ class TestSourceParsingAndCompiling(object):
|
||||
assert str(source) == "x=3"
|
||||
|
||||
def test_compile_and_getsource_through_same_function(self):
|
||||
|
||||
def gensource(source):
|
||||
return _pytest._code.compile(source)
|
||||
|
||||
@@ -337,13 +333,12 @@ class TestSourceParsingAndCompiling(object):
|
||||
|
||||
@pytest.mark.parametrize("name", ["", None, "my"])
|
||||
def test_compilefuncs_and_path_sanity(self, name):
|
||||
|
||||
def check(comp, name):
|
||||
co = comp(self.source, name)
|
||||
if not name:
|
||||
expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 3)
|
||||
expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2)
|
||||
else:
|
||||
expected = "codegen %r %s:%d>" % (name, mypath, mylineno + 2 + 3)
|
||||
expected = "codegen %r %s:%d>" % (name, mypath, mylineno + 2 + 2)
|
||||
fn = co.co_filename
|
||||
assert fn.endswith(expected)
|
||||
|
||||
@@ -359,9 +354,7 @@ class TestSourceParsingAndCompiling(object):
|
||||
|
||||
|
||||
def test_getstartingblock_singleline():
|
||||
|
||||
class A(object):
|
||||
|
||||
def __init__(self, *args):
|
||||
frame = sys._getframe(1)
|
||||
self.source = _pytest._code.Frame(frame).statement
|
||||
@@ -373,7 +366,6 @@ def test_getstartingblock_singleline():
|
||||
|
||||
|
||||
def test_getline_finally():
|
||||
|
||||
def c():
|
||||
pass
|
||||
|
||||
@@ -406,7 +398,6 @@ def test_getfuncsource_dynamic():
|
||||
|
||||
|
||||
def test_getfuncsource_with_multine_string():
|
||||
|
||||
def f():
|
||||
c = """while True:
|
||||
pass
|
||||
@@ -538,14 +529,12 @@ def test_getfslineno():
|
||||
|
||||
|
||||
def test_code_of_object_instance_with_call():
|
||||
|
||||
class A(object):
|
||||
pass
|
||||
|
||||
pytest.raises(TypeError, lambda: _pytest._code.Source(A()))
|
||||
|
||||
class WithCall(object):
|
||||
|
||||
def __call__(self):
|
||||
pass
|
||||
|
||||
@@ -553,7 +542,6 @@ def test_code_of_object_instance_with_call():
|
||||
assert "pass" in str(code.source())
|
||||
|
||||
class Hello(object):
|
||||
|
||||
def __call__(self):
|
||||
pass
|
||||
|
||||
@@ -756,3 +744,19 @@ something
|
||||
'''"""
|
||||
result = getstatement(1, source)
|
||||
assert str(result) == "'''\n'''"
|
||||
|
||||
|
||||
def test_getstartingblock_multiline():
|
||||
class A(object):
|
||||
def __init__(self, *args):
|
||||
frame = sys._getframe(1)
|
||||
self.source = _pytest._code.Frame(frame).statement
|
||||
|
||||
# fmt: off
|
||||
x = A('x',
|
||||
'y'
|
||||
,
|
||||
'z')
|
||||
# fmt: on
|
||||
values = [i for i in x.source.lines if i.strip()]
|
||||
assert len(values) == 4
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# flake8: noqa
|
||||
import sys
|
||||
|
||||
import _pytest._code
|
||||
|
||||
|
||||
def test_getstartingblock_multiline():
|
||||
"""
|
||||
This test was originally found in test_source.py, but it depends on the weird
|
||||
formatting of the ``x = A`` construct seen here and our autopep8 tool can only exclude entire
|
||||
files (it does not support excluding lines/blocks using the traditional #noqa comment yet,
|
||||
see hhatto/autopep8#307). It was considered better to just move this single test to its own
|
||||
file and exclude it from autopep8 than try to complicate things.
|
||||
"""
|
||||
|
||||
class A(object):
|
||||
|
||||
def __init__(self, *args):
|
||||
frame = sys._getframe(1)
|
||||
self.source = _pytest._code.Frame(frame).statement
|
||||
|
||||
# fmt: off
|
||||
x = A('x',
|
||||
'y'
|
||||
,
|
||||
'z')
|
||||
# fmt: on
|
||||
values = [i for i in x.source.lines if i.strip()]
|
||||
assert len(values) == 4
|
||||
@@ -1,4 +1,6 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -84,7 +86,6 @@ def test_str_args_deprecated(tmpdir, testdir):
|
||||
warnings = []
|
||||
|
||||
class Collect(object):
|
||||
|
||||
def pytest_logwarning(self, message):
|
||||
warnings.append(message)
|
||||
|
||||
@@ -260,6 +261,18 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_false_positives(
|
||||
)
|
||||
res = testdir.runpytest_subprocess()
|
||||
assert res.ret == 0
|
||||
assert str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[
|
||||
0
|
||||
] not in res.stderr.str()
|
||||
assert (
|
||||
str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
|
||||
not in res.stderr.str()
|
||||
)
|
||||
|
||||
|
||||
def test_call_fixture_function_deprecated():
|
||||
"""Check if a warning is raised if a fixture function is called directly (#3661)"""
|
||||
|
||||
@pytest.fixture
|
||||
def fix():
|
||||
return 1
|
||||
|
||||
with pytest.deprecated_call():
|
||||
assert fix() == 1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user