sitcod3 commited on
Commit
6b7c45c
·
verified ·
1 Parent(s): 0edf2ad

cleanup: remove venv from image repo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. venv/.gitignore +0 -2
  2. venv/Lib/site-packages/PySocks-1.7.1.dist-info/INSTALLER +0 -1
  3. venv/Lib/site-packages/PySocks-1.7.1.dist-info/LICENSE +0 -22
  4. venv/Lib/site-packages/PySocks-1.7.1.dist-info/METADATA +0 -321
  5. venv/Lib/site-packages/PySocks-1.7.1.dist-info/RECORD +0 -11
  6. venv/Lib/site-packages/PySocks-1.7.1.dist-info/REQUESTED +0 -0
  7. venv/Lib/site-packages/PySocks-1.7.1.dist-info/WHEEL +0 -5
  8. venv/Lib/site-packages/PySocks-1.7.1.dist-info/top_level.txt +0 -2
  9. venv/Lib/site-packages/StrEnum-0.4.15.dist-info/INSTALLER +0 -1
  10. venv/Lib/site-packages/StrEnum-0.4.15.dist-info/LICENSE +0 -21
  11. venv/Lib/site-packages/StrEnum-0.4.15.dist-info/METADATA +0 -211
  12. venv/Lib/site-packages/StrEnum-0.4.15.dist-info/RECORD +0 -18
  13. venv/Lib/site-packages/StrEnum-0.4.15.dist-info/WHEEL +0 -5
  14. venv/Lib/site-packages/StrEnum-0.4.15.dist-info/top_level.txt +0 -1
  15. venv/Lib/site-packages/_cffi_backend.cp314-win_amd64.pyd +0 -3
  16. venv/Lib/site-packages/_pytest/__init__.py +0 -13
  17. venv/Lib/site-packages/_pytest/_argcomplete.py +0 -117
  18. venv/Lib/site-packages/_pytest/_code/__init__.py +0 -26
  19. venv/Lib/site-packages/_pytest/_code/code.py +0 -1632
  20. venv/Lib/site-packages/_pytest/_code/source.py +0 -228
  21. venv/Lib/site-packages/_pytest/_io/__init__.py +0 -10
  22. venv/Lib/site-packages/_pytest/_io/pprint.py +0 -673
  23. venv/Lib/site-packages/_pytest/_io/saferepr.py +0 -155
  24. venv/Lib/site-packages/_pytest/_io/terminalwriter.py +0 -258
  25. venv/Lib/site-packages/_pytest/_io/wcwidth.py +0 -57
  26. venv/Lib/site-packages/_pytest/_py/__init__.py +0 -0
  27. venv/Lib/site-packages/_pytest/_py/error.py +0 -119
  28. venv/Lib/site-packages/_pytest/_py/path.py +0 -1475
  29. venv/Lib/site-packages/_pytest/_version.py +0 -24
  30. venv/Lib/site-packages/_pytest/assertion/__init__.py +0 -236
  31. venv/Lib/site-packages/_pytest/assertion/_compare_any.py +0 -131
  32. venv/Lib/site-packages/_pytest/assertion/_compare_mapping.py +0 -48
  33. venv/Lib/site-packages/_pytest/assertion/_compare_sequence.py +0 -90
  34. venv/Lib/site-packages/_pytest/assertion/_compare_set.py +0 -101
  35. venv/Lib/site-packages/_pytest/assertion/_guards.py +0 -60
  36. venv/Lib/site-packages/_pytest/assertion/_typing.py +0 -12
  37. venv/Lib/site-packages/_pytest/assertion/compare_text.py +0 -104
  38. venv/Lib/site-packages/_pytest/assertion/highlight.py +0 -11
  39. venv/Lib/site-packages/_pytest/assertion/rewrite.py +0 -1193
  40. venv/Lib/site-packages/_pytest/assertion/truncate.py +0 -128
  41. venv/Lib/site-packages/_pytest/assertion/util.py +0 -215
  42. venv/Lib/site-packages/_pytest/cacheprovider.py +0 -640
  43. venv/Lib/site-packages/_pytest/capture.py +0 -1151
  44. venv/Lib/site-packages/_pytest/compat.py +0 -329
  45. venv/Lib/site-packages/_pytest/config/__init__.py +0 -2246
  46. venv/Lib/site-packages/_pytest/config/argparsing.py +0 -503
  47. venv/Lib/site-packages/_pytest/config/exceptions.py +0 -15
  48. venv/Lib/site-packages/_pytest/config/findpaths.py +0 -350
  49. venv/Lib/site-packages/_pytest/debugging.py +0 -404
  50. venv/Lib/site-packages/_pytest/deprecated.py +0 -139
venv/.gitignore DELETED
@@ -1,2 +0,0 @@
1
- # Created by venv; see https://docs.python.org/3/library/venv.html
2
- *
 
 
 
venv/Lib/site-packages/PySocks-1.7.1.dist-info/INSTALLER DELETED
@@ -1 +0,0 @@
1
- pip
 
 
venv/Lib/site-packages/PySocks-1.7.1.dist-info/LICENSE DELETED
@@ -1,22 +0,0 @@
1
- Copyright 2006 Dan-Haim. All rights reserved.
2
-
3
- Redistribution and use in source and binary forms, with or without modification,
4
- are permitted provided that the following conditions are met:
5
- 1. Redistributions of source code must retain the above copyright notice, this
6
- list of conditions and the following disclaimer.
7
- 2. Redistributions in binary form must reproduce the above copyright notice,
8
- this list of conditions and the following disclaimer in the documentation
9
- and/or other materials provided with the distribution.
10
- 3. Neither the name of Dan Haim nor the names of his contributors may be used
11
- to endorse or promote products derived from this software without specific
12
- prior written permission.
13
-
14
- THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
15
- WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
- MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
17
- EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
18
- INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
19
- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
20
- OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
21
- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
22
- OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/PySocks-1.7.1.dist-info/METADATA DELETED
@@ -1,321 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: PySocks
3
- Version: 1.7.1
4
- Summary: A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information.
5
- Home-page: https://github.com/Anorov/PySocks
6
- Author: Anorov
7
- Author-email: anorov.vorona@gmail.com
8
- License: BSD
9
- Keywords: socks,proxy
10
- Platform: UNKNOWN
11
- Classifier: Programming Language :: Python :: 2
12
- Classifier: Programming Language :: Python :: 2.7
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.4
15
- Classifier: Programming Language :: Python :: 3.5
16
- Classifier: Programming Language :: Python :: 3.6
17
- Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
18
- Description-Content-Type: text/markdown
19
-
20
- PySocks
21
- =======
22
-
23
- PySocks lets you send traffic through SOCKS and HTTP proxy servers. It is a modern fork of [SocksiPy](http://socksipy.sourceforge.net/) with bug fixes and extra features.
24
-
25
- Acts as a drop-in replacement to the socket module. Seamlessly configure SOCKS proxies for any socket object by calling `socket_object.set_proxy()`.
26
-
27
- ----------------
28
-
29
- Features
30
- ========
31
-
32
- * SOCKS proxy client for Python 2.7 and 3.4+
33
- * TCP supported
34
- * UDP mostly supported (issues may occur in some edge cases)
35
- * HTTP proxy client included but not supported or recommended (you should use urllib2's or requests' own HTTP proxy interface)
36
- * urllib2 handler included. `pip install` / `setup.py install` will automatically install the `sockshandler` module.
37
-
38
- Installation
39
- ============
40
-
41
- pip install PySocks
42
-
43
- Or download the tarball / `git clone` and...
44
-
45
- python setup.py install
46
-
47
- These will install both the `socks` and `sockshandler` modules.
48
-
49
- Alternatively, include just `socks.py` in your project.
50
-
51
- --------------------------------------------
52
-
53
- *Warning:* PySocks/SocksiPy only supports HTTP proxies that use CONNECT tunneling. Certain HTTP proxies may not work with this library. If you wish to use HTTP (not SOCKS) proxies, it is recommended that you rely on your HTTP client's native proxy support (`proxies` dict for `requests`, or `urllib2.ProxyHandler` for `urllib2`) instead.
54
-
55
- --------------------------------------------
56
-
57
- Usage
58
- =====
59
-
60
- ## socks.socksocket ##
61
-
62
- import socks
63
-
64
- s = socks.socksocket() # Same API as socket.socket in the standard lib
65
-
66
- s.set_proxy(socks.SOCKS5, "localhost") # SOCKS4 and SOCKS5 use port 1080 by default
67
- # Or
68
- s.set_proxy(socks.SOCKS4, "localhost", 4444)
69
- # Or
70
- s.set_proxy(socks.HTTP, "5.5.5.5", 8888)
71
-
72
- # Can be treated identical to a regular socket object
73
- s.connect(("www.somesite.com", 80))
74
- s.sendall("GET / HTTP/1.1 ...")
75
- print s.recv(4096)
76
-
77
- ## Monkeypatching ##
78
-
79
- To monkeypatch the entire standard library with a single default proxy:
80
-
81
- import urllib2
82
- import socket
83
- import socks
84
-
85
- socks.set_default_proxy(socks.SOCKS5, "localhost")
86
- socket.socket = socks.socksocket
87
-
88
- urllib2.urlopen("http://www.somesite.com/") # All requests will pass through the SOCKS proxy
89
-
90
- Note that monkeypatching may not work for all standard modules or for all third party modules, and generally isn't recommended. Monkeypatching is usually an anti-pattern in Python.
91
-
92
- ## urllib2 Handler ##
93
-
94
- Example use case with the `sockshandler` urllib2 handler. Note that you must import both `socks` and `sockshandler`, as the handler is its own module separate from PySocks. The module is included in the PyPI package.
95
-
96
- import urllib2
97
- import socks
98
- from sockshandler import SocksiPyHandler
99
-
100
- opener = urllib2.build_opener(SocksiPyHandler(socks.SOCKS5, "127.0.0.1", 9050))
101
- print opener.open("http://www.somesite.com/") # All requests made by the opener will pass through the SOCKS proxy
102
-
103
- --------------------------------------------
104
-
105
- Original SocksiPy README attached below, amended to reflect API changes.
106
-
107
- --------------------------------------------
108
-
109
- SocksiPy
110
-
111
- A Python SOCKS module.
112
-
113
- (C) 2006 Dan-Haim. All rights reserved.
114
-
115
- See LICENSE file for details.
116
-
117
-
118
- *WHAT IS A SOCKS PROXY?*
119
-
120
- A SOCKS proxy is a proxy server at the TCP level. In other words, it acts as
121
- a tunnel, relaying all traffic going through it without modifying it.
122
- SOCKS proxies can be used to relay traffic using any network protocol that
123
- uses TCP.
124
-
125
- *WHAT IS SOCKSIPY?*
126
-
127
- This Python module allows you to create TCP connections through a SOCKS
128
- proxy without any special effort.
129
- It also supports relaying UDP packets with a SOCKS5 proxy.
130
-
131
- *PROXY COMPATIBILITY*
132
-
133
- SocksiPy is compatible with three different types of proxies:
134
-
135
- 1. SOCKS Version 4 (SOCKS4), including the SOCKS4a extension.
136
- 2. SOCKS Version 5 (SOCKS5).
137
- 3. HTTP Proxies which support tunneling using the CONNECT method.
138
-
139
- *SYSTEM REQUIREMENTS*
140
-
141
- Being written in Python, SocksiPy can run on any platform that has a Python
142
- interpreter and TCP/IP support.
143
- This module has been tested with Python 2.3 and should work with greater versions
144
- just as well.
145
-
146
-
147
- INSTALLATION
148
- -------------
149
-
150
- Simply copy the file "socks.py" to your Python's `lib/site-packages` directory,
151
- and you're ready to go. [Editor's note: it is better to use `python setup.py install` for PySocks]
152
-
153
-
154
- USAGE
155
- ------
156
-
157
- First load the socks module with the command:
158
-
159
- >>> import socks
160
- >>>
161
-
162
- The socks module provides a class called `socksocket`, which is the base to all of the module's functionality.
163
-
164
- The `socksocket` object has the same initialization parameters as the normal socket
165
- object to ensure maximal compatibility, however it should be noted that `socksocket` will only function with family being `AF_INET` and
166
- type being either `SOCK_STREAM` or `SOCK_DGRAM`.
167
- Generally, it is best to initialize the `socksocket` object with no parameters
168
-
169
- >>> s = socks.socksocket()
170
- >>>
171
-
172
- The `socksocket` object has an interface which is very similiar to socket's (in fact
173
- the `socksocket` class is derived from socket) with a few extra methods.
174
- To select the proxy server you would like to use, use the `set_proxy` method, whose
175
- syntax is:
176
-
177
- set_proxy(proxy_type, addr[, port[, rdns[, username[, password]]]])
178
-
179
- Explanation of the parameters:
180
-
181
- `proxy_type` - The type of the proxy server. This can be one of three possible
182
- choices: `PROXY_TYPE_SOCKS4`, `PROXY_TYPE_SOCKS5` and `PROXY_TYPE_HTTP` for SOCKS4,
183
- SOCKS5 and HTTP servers respectively. `SOCKS4`, `SOCKS5`, and `HTTP` are all aliases, respectively.
184
-
185
- `addr` - The IP address or DNS name of the proxy server.
186
-
187
- `port` - The port of the proxy server. Defaults to 1080 for socks and 8080 for http.
188
-
189
- `rdns` - This is a boolean flag than modifies the behavior regarding DNS resolving.
190
- If it is set to True, DNS resolving will be preformed remotely, on the server.
191
- If it is set to False, DNS resolving will be preformed locally. Please note that
192
- setting this to True with SOCKS4 servers actually use an extension to the protocol,
193
- called SOCKS4a, which may not be supported on all servers (SOCKS5 and http servers
194
- always support DNS). The default is True.
195
-
196
- `username` - For SOCKS5 servers, this allows simple username / password authentication
197
- with the server. For SOCKS4 servers, this parameter will be sent as the userid.
198
- This parameter is ignored if an HTTP server is being used. If it is not provided,
199
- authentication will not be used (servers may accept unauthenticated requests).
200
-
201
- `password` - This parameter is valid only for SOCKS5 servers and specifies the
202
- respective password for the username provided.
203
-
204
- Example of usage:
205
-
206
- >>> s.set_proxy(socks.SOCKS5, "socks.example.com") # uses default port 1080
207
- >>> s.set_proxy(socks.SOCKS4, "socks.test.com", 1081)
208
-
209
- After the set_proxy method has been called, simply call the connect method with the
210
- traditional parameters to establish a connection through the proxy:
211
-
212
- >>> s.connect(("www.sourceforge.net", 80))
213
- >>>
214
-
215
- Connection will take a bit longer to allow negotiation with the proxy server.
216
- Please note that calling connect without calling `set_proxy` earlier will connect
217
- without a proxy (just like a regular socket).
218
-
219
- Errors: Any errors in the connection process will trigger exceptions. The exception
220
- may either be generated by the underlying socket layer or may be custom module
221
- exceptions, whose details follow:
222
-
223
- class `ProxyError` - This is a base exception class. It is not raised directly but
224
- rather all other exception classes raised by this module are derived from it.
225
- This allows an easy way to catch all proxy-related errors. It descends from `IOError`.
226
-
227
- All `ProxyError` exceptions have an attribute `socket_err`, which will contain either a
228
- caught `socket.error` exception, or `None` if there wasn't any.
229
-
230
- class `GeneralProxyError` - When thrown, it indicates a problem which does not fall
231
- into another category.
232
-
233
- * `Sent invalid data` - This error means that unexpected data has been received from
234
- the server. The most common reason is that the server specified as the proxy is
235
- not really a SOCKS4/SOCKS5/HTTP proxy, or maybe the proxy type specified is wrong.
236
-
237
- * `Connection closed unexpectedly` - The proxy server unexpectedly closed the connection.
238
- This may indicate that the proxy server is experiencing network or software problems.
239
-
240
- * `Bad proxy type` - This will be raised if the type of the proxy supplied to the
241
- set_proxy function was not one of `SOCKS4`/`SOCKS5`/`HTTP`.
242
-
243
- * `Bad input` - This will be raised if the `connect()` method is called with bad input
244
- parameters.
245
-
246
- class `SOCKS5AuthError` - This indicates that the connection through a SOCKS5 server
247
- failed due to an authentication problem.
248
-
249
- * `Authentication is required` - This will happen if you use a SOCKS5 server which
250
- requires authentication without providing a username / password at all.
251
-
252
- * `All offered authentication methods were rejected` - This will happen if the proxy
253
- requires a special authentication method which is not supported by this module.
254
-
255
- * `Unknown username or invalid password` - Self descriptive.
256
-
257
- class `SOCKS5Error` - This will be raised for SOCKS5 errors which are not related to
258
- authentication.
259
- The parameter is a tuple containing a code, as given by the server,
260
- and a description of the
261
- error. The possible errors, according to the RFC, are:
262
-
263
- * `0x01` - General SOCKS server failure - If for any reason the proxy server is unable to
264
- fulfill your request (internal server error).
265
- * `0x02` - connection not allowed by ruleset - If the address you're trying to connect to
266
- is blacklisted on the server or requires authentication.
267
- * `0x03` - Network unreachable - The target could not be contacted. A router on the network
268
- had replied with a destination net unreachable error.
269
- * `0x04` - Host unreachable - The target could not be contacted. A router on the network
270
- had replied with a destination host unreachable error.
271
- * `0x05` - Connection refused - The target server has actively refused the connection
272
- (the requested port is closed).
273
- * `0x06` - TTL expired - The TTL value of the SYN packet from the proxy to the target server
274
- has expired. This usually means that there are network problems causing the packet
275
- to be caught in a router-to-router "ping-pong".
276
- * `0x07` - Command not supported - For instance if the server does not support UDP.
277
- * `0x08` - Address type not supported - The client has provided an invalid address type.
278
- When using this module, this error should not occur.
279
-
280
- class `SOCKS4Error` - This will be raised for SOCKS4 errors. The parameter is a tuple
281
- containing a code and a description of the error, as given by the server. The
282
- possible error, according to the specification are:
283
-
284
- * `0x5B` - Request rejected or failed - Will be raised in the event of an failure for any
285
- reason other then the two mentioned next.
286
- * `0x5C` - request rejected because SOCKS server cannot connect to identd on the client -
287
- The Socks server had tried an ident lookup on your computer and has failed. In this
288
- case you should run an identd server and/or configure your firewall to allow incoming
289
- connections to local port 113 from the remote server.
290
- * `0x5D` - request rejected because the client program and identd report different user-ids -
291
- The Socks server had performed an ident lookup on your computer and has received a
292
- different userid than the one you have provided. Change your userid (through the
293
- username parameter of the set_proxy method) to match and try again.
294
-
295
- class `HTTPError` - This will be raised for HTTP errors. The message will contain
296
- the HTTP status code and provided error message.
297
-
298
- After establishing the connection, the object behaves like a standard socket.
299
-
300
- Methods like `makefile()` and `settimeout()` should behave just like regular sockets.
301
- Call the `close()` method to close the connection.
302
-
303
- In addition to the `socksocket` class, an additional function worth mentioning is the
304
- `set_default_proxy` function. The parameters are the same as the `set_proxy` method.
305
- This function will set default proxy settings for newly created `socksocket` objects,
306
- in which the proxy settings haven't been changed via the `set_proxy` method.
307
- This is quite useful if you wish to force 3rd party modules to use a SOCKS proxy,
308
- by overriding the socket object.
309
- For example:
310
-
311
- >>> socks.set_default_proxy(socks.SOCKS5, "socks.example.com")
312
- >>> socket.socket = socks.socksocket
313
- >>> urllib.urlopen("http://www.sourceforge.net/")
314
-
315
-
316
- PROBLEMS
317
- ---------
318
-
319
- Please open a GitHub issue at https://github.com/Anorov/PySocks
320
-
321
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/PySocks-1.7.1.dist-info/RECORD DELETED
@@ -1,11 +0,0 @@
1
- PySocks-1.7.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
2
- PySocks-1.7.1.dist-info/LICENSE,sha256=cCfiFOAU63i3rcwc7aWspxOnn8T2oMUsnaWz5wfm_-k,1401
3
- PySocks-1.7.1.dist-info/METADATA,sha256=zbQMizjPOOP4DhEiEX24XXjNrYuIxF9UGUpN0uFDB6Y,13235
4
- PySocks-1.7.1.dist-info/RECORD,,
5
- PySocks-1.7.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- PySocks-1.7.1.dist-info/WHEEL,sha256=t_MpApv386-8PVts2R6wsTifdIn0vbUDTVv61IbqFC8,92
7
- PySocks-1.7.1.dist-info/top_level.txt,sha256=TKSOIfCFBoK9EY8FBYbYqC3PWd3--G15ph9n8-QHPDk,19
8
- __pycache__/socks.cpython-314.pyc,,
9
- __pycache__/sockshandler.cpython-314.pyc,,
10
- socks.py,sha256=xOYn27t9IGrbTBzWsUUuPa0YBuplgiUykzkOB5V5iFY,31086
11
- sockshandler.py,sha256=2SYGj-pwt1kjgLoZAmyeaEXCeZDWRmfVS_QG6kErGtY,3966
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/PySocks-1.7.1.dist-info/REQUESTED DELETED
File without changes
venv/Lib/site-packages/PySocks-1.7.1.dist-info/WHEEL DELETED
@@ -1,5 +0,0 @@
1
- Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.33.3)
3
- Root-Is-Purelib: true
4
- Tag: py3-none-any
5
-
 
 
 
 
 
 
venv/Lib/site-packages/PySocks-1.7.1.dist-info/top_level.txt DELETED
@@ -1,2 +0,0 @@
1
- socks
2
- sockshandler
 
 
 
venv/Lib/site-packages/StrEnum-0.4.15.dist-info/INSTALLER DELETED
@@ -1 +0,0 @@
1
- pip
 
 
venv/Lib/site-packages/StrEnum-0.4.15.dist-info/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2019 James C Sinclair
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/StrEnum-0.4.15.dist-info/METADATA DELETED
@@ -1,211 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: StrEnum
3
- Version: 0.4.15
4
- Summary: An Enum that inherits from str.
5
- Home-page: https://github.com/irgeek/StrEnum
6
- Author: James Sinclair
7
- Author-email: james@nurfherder.com
8
- Classifier: Development Status :: 5 - Production/Stable
9
- Classifier: Programming Language :: Python :: 3 :: Only
10
- Classifier: Programming Language :: Python :: 3.7
11
- Classifier: Programming Language :: Python :: 3.8
12
- Classifier: Programming Language :: Python :: 3.9
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Classifier: License :: OSI Approved :: MIT License
17
- Classifier: Operating System :: OS Independent
18
- Description-Content-Type: text/markdown
19
- License-File: LICENSE
20
- Provides-Extra: docs
21
- Requires-Dist: sphinx ; extra == 'docs'
22
- Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
23
- Requires-Dist: myst-parser[linkify] ; extra == 'docs'
24
- Provides-Extra: release
25
- Requires-Dist: twine ; extra == 'release'
26
- Provides-Extra: test
27
- Requires-Dist: pytest ; extra == 'test'
28
- Requires-Dist: pytest-black ; extra == 'test'
29
- Requires-Dist: pytest-cov ; extra == 'test'
30
- Requires-Dist: pytest-pylint ; extra == 'test'
31
- Requires-Dist: pylint ; extra == 'test'
32
-
33
- # StrEnum
34
-
35
- [![Build Status](https://github.com/irgeek/StrEnum/workflows/Python%20package/badge.svg)](https://github.com/irgeek/StrEnum/actions)
36
-
37
- StrEnum is a Python `enum.Enum` that inherits from `str` to complement
38
- `enum.IntEnum` in the standard library. Supports python 3.7+.
39
-
40
- ## Installation
41
-
42
- You can use [pip](https://pip.pypa.io/en/stable/) to install.
43
-
44
- ```bash
45
- pip install StrEnum
46
- ```
47
-
48
- ## Usage
49
-
50
- ```python
51
- from enum import auto
52
- from strenum import StrEnum
53
-
54
-
55
- class HttpMethod(StrEnum):
56
- GET = auto()
57
- HEAD = auto()
58
- POST = auto()
59
- PUT = auto()
60
- DELETE = auto()
61
- CONNECT = auto()
62
- OPTIONS = auto()
63
- TRACE = auto()
64
- PATCH = auto()
65
-
66
-
67
- assert HttpMethod.GET == "GET"
68
-
69
- # You can use StrEnum values just like strings:
70
-
71
- import urllib.request
72
-
73
- req = urllib.request.Request('https://www.python.org/', method=HttpMethod.HEAD)
74
- with urllib.request.urlopen(req) as response:
75
- html = response.read()
76
-
77
- assert len(html) == 0 # HEAD requests do not (usually) include a body
78
- ```
79
-
80
- There are classes whose `auto()` value folds each member name to upper or lower
81
- case:
82
-
83
- ```python
84
- from enum import auto
85
- from strenum import LowercaseStrEnum, UppercaseStrEnum
86
-
87
-
88
- class Tag(LowercaseStrEnum):
89
- Head = auto()
90
- Body = auto()
91
- Div = auto()
92
-
93
-
94
- assert Tag.Head == "head"
95
- assert Tag.Body == "body"
96
- assert Tag.Div == "div"
97
-
98
-
99
- class HttpMethod(UppercaseStrEnum):
100
- Get = auto()
101
- Head = auto()
102
- Post = auto()
103
-
104
-
105
- assert HttpMethod.Get == "GET"
106
- assert HttpMethod.Head == "HEAD"
107
- assert HttpMethod.Post == "POST"
108
- ```
109
-
110
- As well as classes whose `auto()` value converts each member name to camelCase,
111
- PascalCase, kebab-case, snake_case and MACRO_CASE:
112
-
113
- ```python
114
- from enum import auto
115
- from strenum import CamelCaseStrEnum, PascalCaseStrEnum
116
- from strenum import KebabCaseStrEnum, SnakeCaseStrEnum
117
- from strenum import MacroCaseStrEnum
118
-
119
-
120
- class CamelTestEnum(CamelCaseStrEnum):
121
- OneTwoThree = auto()
122
-
123
-
124
- class PascalTestEnum(PascalCaseStrEnum):
125
- OneTwoThree = auto()
126
-
127
-
128
- class KebabTestEnum(KebabCaseStrEnum):
129
- OneTwoThree = auto()
130
-
131
-
132
- class SnakeTestEnum(SnakeCaseStrEnum):
133
- OneTwoThree = auto()
134
-
135
-
136
- class MacroTestEnum(MacroCaseStrEnum):
137
- OneTwoThree = auto()
138
-
139
-
140
- assert CamelTestEnum.OneTwoThree == "oneTwoThree"
141
- assert PascalTestEnum.OneTwoThree == "OneTwoThree"
142
- assert KebabTestEnum.OneTwoThree == "one-two-three"
143
- assert SnakeTestEnum.OneTwoThree == "one_two_three"
144
- assert MacroTestEnum.OneTwoThree == "ONE_TWO_THREE"
145
- ```
146
-
147
- As with any Enum you can, of course, manually assign values.
148
-
149
- ```python
150
- from strenum import StrEnum
151
-
152
-
153
- class Shape(StrEnum):
154
- CIRCLE = "Circle"
155
-
156
-
157
- assert Shape.CIRCLE == "Circle"
158
- ```
159
-
160
- Doing this with the case-changing classes, though, won't manipulate
161
- values--whatever you assign is the value they end up with.
162
-
163
- ```python
164
- from strenum import KebabCaseStrEnum
165
-
166
-
167
- class Shape(KebabCaseStrEnum):
168
- CIRCLE = "Circle"
169
-
170
-
171
- # This will raise an AssertionError because the value wasn't converted to kebab-case.
172
- assert Shape.CIRCLE == "circle"
173
- ```
174
-
175
- ## Contributing
176
-
177
- Pull requests are welcome. For major changes, please open an issue first to
178
- discuss what you would like to change.
179
-
180
- Please ensure tests pass before submitting a PR. This repository uses
181
- [Black](https://black.readthedocs.io/en/stable/) and
182
- [Pylint](https://www.pylint.org/) for consistency. Both are run automatically
183
- as part of the test suite.
184
-
185
- ## Running the tests
186
-
187
- Tests can be run using `make`:
188
-
189
- ```
190
- make test
191
- ```
192
-
193
- This will create a virutal environment, install the module and its test
194
- dependencies and run the tests. Alternatively you can do the same thing
195
- manually:
196
-
197
- ```
198
- python3 -m venv .venv
199
- .venv/bin/pip install .[test]
200
- .venv/bin/pytest
201
- ```
202
-
203
- ## License
204
-
205
- [MIT](https://choosealicense.com/licenses/mit/)
206
-
207
- **N.B. Starting with Python 3.11, `enum.StrEnum` is available in the standard
208
- library. This implementation is _not_ a drop-in replacement for the standard
209
- library implementation. Specifically, the Python devs have decided to case fold
210
- name to lowercase by default when `auto()` is used which I think violates the
211
- principle of least surprise.**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/StrEnum-0.4.15.dist-info/RECORD DELETED
@@ -1,18 +0,0 @@
1
- StrEnum-0.4.15.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
2
- StrEnum-0.4.15.dist-info/LICENSE,sha256=vNcz0KRlIhYrldurYffNwcPjaGHfoSfWikQ1JA02rTY,1073
3
- StrEnum-0.4.15.dist-info/METADATA,sha256=xLhPNsn2ieV9BR8L-FvFTMMhG6x99Rj536PM2hf1KII,5290
4
- StrEnum-0.4.15.dist-info/RECORD,,
5
- StrEnum-0.4.15.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
6
- StrEnum-0.4.15.dist-info/top_level.txt,sha256=lsVIlgvvAAG9MBoLNwpPgG5wMT33blIUUoJo215Y-N0,8
7
- strenum/__init__.py,sha256=oOsokqYYQVwTBbjrnXdR89RMcx8q6aGmWl--dlf2VCE,8530
8
- strenum/__init__.pyi,sha256=-qJs9THlVGY3_o_bVeE7jUevjWteU_zOy7cZkp3MGzA,1415
9
- strenum/__pycache__/__init__.cpython-314.pyc,,
10
- strenum/__pycache__/_name_mangler.cpython-314.pyc,,
11
- strenum/__pycache__/_version.cpython-314.pyc,,
12
- strenum/__pycache__/mixins.cpython-314.pyc,,
13
- strenum/_name_mangler.py,sha256=o11M5-bURW2RBvRTYXFQIPNeqLzburdoWLIqk8X3ydw,3397
14
- strenum/_name_mangler.pyi,sha256=91p30d_kMAFsX5r9pqvSeordIf4VONDymQhcEvot2XA,551
15
- strenum/_version.py,sha256=ylMBlzCLrUBhNvBnKtzTqGzDqHnEmoQkm3K6p5Gx5ms,498
16
- strenum/mixins.py,sha256=BtOEx1hrAZ1YhDURDjLTrunKVneVtezSfkBq3sZXqxE,2042
17
- strenum/mixins.pyi,sha256=lV6wiQAxZMvMSUUtnxzV1QSsyBDzgpobywiGCwRwm30,387
18
- strenum/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/StrEnum-0.4.15.dist-info/WHEEL DELETED
@@ -1,5 +0,0 @@
1
- Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.40.0)
3
- Root-Is-Purelib: true
4
- Tag: py3-none-any
5
-
 
 
 
 
 
 
venv/Lib/site-packages/StrEnum-0.4.15.dist-info/top_level.txt DELETED
@@ -1 +0,0 @@
1
- strenum
 
 
venv/Lib/site-packages/_cffi_backend.cp314-win_amd64.pyd DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:9e99b2184c60f21a88cafc374386ded3559e8694ed7cf111f2869e066d303334
3
- size 181248
 
 
 
 
venv/Lib/site-packages/_pytest/__init__.py DELETED
@@ -1,13 +0,0 @@
1
- from __future__ import annotations
2
-
3
-
4
- __all__ = ["__version__", "version_tuple"]
5
-
6
- try:
7
- from ._version import version as __version__
8
- from ._version import version_tuple
9
- except ImportError: # pragma: no cover
10
- # broken installation, we don't even try
11
- # unknown only works because we do poor mans version compare
12
- __version__ = "unknown"
13
- version_tuple = (0, 0, "unknown")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_argcomplete.py DELETED
@@ -1,117 +0,0 @@
1
- """Allow bash-completion for argparse with argcomplete if installed.
2
-
3
- Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
4
- to find the magic string, so _ARGCOMPLETE env. var is never set, and
5
- this does not need special code).
6
-
7
- Function try_argcomplete(parser) should be called directly before
8
- the call to ArgumentParser.parse_args().
9
-
10
- The filescompleter is what you normally would use on the positional
11
- arguments specification, in order to get "dirname/" after "dirn<TAB>"
12
- instead of the default "dirname ":
13
-
14
- optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter
15
-
16
- Other, application specific, completers should go in the file
17
- doing the add_argument calls as they need to be specified as .completer
18
- attributes as well. (If argcomplete is not installed, the function the
19
- attribute points to will not be used).
20
-
21
- SPEEDUP
22
- =======
23
-
24
- The generic argcomplete script for bash-completion
25
- (/etc/bash_completion.d/python-argcomplete.sh)
26
- uses a python program to determine startup script generated by pip.
27
- You can speed up completion somewhat by changing this script to include
28
- # PYTHON_ARGCOMPLETE_OK
29
- so the python-argcomplete-check-easy-install-script does not
30
- need to be called to find the entry point of the code and see if that is
31
- marked with PYTHON_ARGCOMPLETE_OK.
32
-
33
- INSTALL/DEBUGGING
34
- =================
35
-
36
- To include this support in another application that has setup.py generated
37
- scripts:
38
-
39
- - Add the line:
40
- # PYTHON_ARGCOMPLETE_OK
41
- near the top of the main python entry point.
42
-
43
- - Include in the file calling parse_args():
44
- from _argcomplete import try_argcomplete, filescompleter
45
- Call try_argcomplete just before parse_args(), and optionally add
46
- filescompleter to the positional arguments' add_argument().
47
-
48
- If things do not work right away:
49
-
50
- - Switch on argcomplete debugging with (also helpful when doing custom
51
- completers):
52
- export _ARC_DEBUG=1
53
-
54
- - Run:
55
- python-argcomplete-check-easy-install-script $(which appname)
56
- echo $?
57
- will echo 0 if the magic line has been found, 1 if not.
58
-
59
- - Sometimes it helps to find early on errors using:
60
- _ARGCOMPLETE=1 _ARC_DEBUG=1 appname
61
- which should throw a KeyError: 'COMPLINE' (which is properly set by the
62
- global argcomplete script).
63
- """
64
-
65
- from __future__ import annotations
66
-
67
- import argparse
68
- from glob import glob
69
- import os
70
- import sys
71
- from typing import Any
72
-
73
-
74
- class FastFilesCompleter:
75
- """Fast file completer class."""
76
-
77
- def __init__(self, directories: bool = True) -> None:
78
- self.directories = directories
79
-
80
- def __call__(self, prefix: str, **kwargs: Any) -> list[str]:
81
- # Only called on non option completions.
82
- if os.sep in prefix[1:]:
83
- prefix_dir = len(os.path.dirname(prefix) + os.sep)
84
- else:
85
- prefix_dir = 0
86
- completion = []
87
- globbed = []
88
- if "*" not in prefix and "?" not in prefix:
89
- # We are on unix, otherwise no bash.
90
- if not prefix or prefix[-1] == os.sep:
91
- globbed.extend(glob(prefix + ".*"))
92
- prefix += "*"
93
- globbed.extend(glob(prefix))
94
- for x in sorted(globbed):
95
- if os.path.isdir(x):
96
- x += "/"
97
- # Append stripping the prefix (like bash, not like compgen).
98
- completion.append(x[prefix_dir:])
99
- return completion
100
-
101
-
102
- if os.environ.get("_ARGCOMPLETE"):
103
- try:
104
- import argcomplete.completers
105
- except ImportError:
106
- sys.exit(-1)
107
- filescompleter: FastFilesCompleter | None = FastFilesCompleter()
108
-
109
- def try_argcomplete(parser: argparse.ArgumentParser) -> None:
110
- argcomplete.autocomplete(parser, always_complete_options=False)
111
-
112
- else:
113
-
114
- def try_argcomplete(parser: argparse.ArgumentParser) -> None:
115
- pass
116
-
117
- filescompleter = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_code/__init__.py DELETED
@@ -1,26 +0,0 @@
1
- """Python inspection/code generation API."""
2
-
3
- from __future__ import annotations
4
-
5
- from .code import Code
6
- from .code import ExceptionInfo
7
- from .code import filter_traceback
8
- from .code import Frame
9
- from .code import getfslineno
10
- from .code import Traceback
11
- from .code import TracebackEntry
12
- from .source import getrawcode
13
- from .source import Source
14
-
15
-
16
- __all__ = [
17
- "Code",
18
- "ExceptionInfo",
19
- "Frame",
20
- "Source",
21
- "Traceback",
22
- "TracebackEntry",
23
- "filter_traceback",
24
- "getfslineno",
25
- "getrawcode",
26
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_code/code.py DELETED
@@ -1,1632 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- from __future__ import annotations
3
-
4
- import ast
5
- from collections.abc import Callable
6
- from collections.abc import Iterable
7
- from collections.abc import Mapping
8
- from collections.abc import Sequence
9
- import dataclasses
10
- import inspect
11
- from io import StringIO
12
- import os
13
- from pathlib import Path
14
- import re
15
- import sys
16
- from traceback import extract_tb
17
- from traceback import format_exception
18
- from traceback import format_exception_only
19
- from traceback import FrameSummary
20
- from types import CodeType
21
- from types import FrameType
22
- from types import TracebackType
23
- from typing import Any
24
- from typing import ClassVar
25
- from typing import Final
26
- from typing import final
27
- from typing import Generic
28
- from typing import Literal
29
- from typing import overload
30
- from typing import SupportsIndex
31
- from typing import TypeAlias
32
- from typing import TypeVar
33
-
34
- import pluggy
35
-
36
- import _pytest
37
- from _pytest._code.source import findsource
38
- from _pytest._code.source import getrawcode
39
- from _pytest._code.source import getstatementrange_ast
40
- from _pytest._code.source import Source
41
- from _pytest._io import TerminalWriter
42
- from _pytest._io.saferepr import safeformat
43
- from _pytest._io.saferepr import saferepr
44
- from _pytest.compat import get_real_func
45
- from _pytest.deprecated import check_ispytest
46
- from _pytest.pathlib import absolutepath
47
- from _pytest.pathlib import bestrelpath
48
-
49
-
50
- if sys.version_info < (3, 11):
51
- from exceptiongroup import BaseExceptionGroup
52
-
53
- TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
54
-
55
- EXCEPTION_OR_MORE = type[BaseException] | tuple[type[BaseException], ...]
56
-
57
-
58
- class Code:
59
- """Wrapper around Python code objects."""
60
-
61
- __slots__ = ("raw",)
62
-
63
- def __init__(self, obj: CodeType) -> None:
64
- self.raw = obj
65
-
66
- @classmethod
67
- def from_function(cls, obj: object) -> Code:
68
- return cls(getrawcode(obj))
69
-
70
- def __eq__(self, other):
71
- return self.raw == other.raw
72
-
73
- # Ignore type because of https://github.com/python/mypy/issues/4266.
74
- __hash__ = None # type: ignore
75
-
76
- @property
77
- def firstlineno(self) -> int:
78
- return self.raw.co_firstlineno - 1
79
-
80
- @property
81
- def name(self) -> str:
82
- return self.raw.co_name
83
-
84
- @property
85
- def path(self) -> Path | str:
86
- """Return a path object pointing to source code, or an ``str`` in
87
- case of ``OSError`` / non-existing file."""
88
- filename = inspect.getfile(self.raw)
89
- if not filename:
90
- return ""
91
- try:
92
- p = absolutepath(filename)
93
- # maybe don't try this checking
94
- if not p.exists():
95
- raise OSError("path check failed.")
96
- return p
97
- except OSError:
98
- # XXX maybe try harder like the weird logic
99
- # in the standard lib [linecache.updatecache] does?
100
- return filename
101
-
102
- @property
103
- def fullsource(self) -> Source | None:
104
- """Return a _pytest._code.Source object for the full source file of the code."""
105
- full, _ = findsource(self.raw)
106
- return full
107
-
108
- def source(self) -> Source:
109
- """Return a _pytest._code.Source object for the code object's source only."""
110
- # return source only for that part of code
111
- return Source(self.raw)
112
-
113
- def getargs(self, var: bool = False) -> tuple[str, ...]:
114
- """Return a tuple with the argument names for the code object.
115
-
116
- If 'var' is set True also return the names of the variable and
117
- keyword arguments when present.
118
- """
119
- # inspect.getargs merges positional and kwonly into a single list;
120
- # co_argcount is needed to exclude kwonly when var=False.
121
- args, varargs, varkw = inspect.getargs(self.raw)
122
- if not var:
123
- return tuple(args[: self.raw.co_argcount])
124
- result = list(args)
125
- if varargs is not None:
126
- result.append(varargs)
127
- if varkw is not None:
128
- result.append(varkw)
129
- return tuple(result)
130
-
131
-
132
- class Frame:
133
- """Wrapper around a Python frame holding f_locals and f_globals
134
- in which expressions can be evaluated."""
135
-
136
- __slots__ = ("raw",)
137
-
138
- def __init__(self, frame: FrameType) -> None:
139
- self.raw = frame
140
-
141
- @property
142
- def lineno(self) -> int:
143
- return self.raw.f_lineno - 1
144
-
145
- @property
146
- def f_globals(self) -> dict[str, Any]:
147
- return self.raw.f_globals
148
-
149
- @property
150
- def f_locals(self) -> dict[str, Any]:
151
- return self.raw.f_locals
152
-
153
- @property
154
- def code(self) -> Code:
155
- return Code(self.raw.f_code)
156
-
157
- @property
158
- def statement(self) -> Source:
159
- """Statement this frame is at."""
160
- if self.code.fullsource is None:
161
- return Source("")
162
- return self.code.fullsource.getstatement(self.lineno)
163
-
164
- def eval(self, code, **vars):
165
- """Evaluate 'code' in the frame.
166
-
167
- 'vars' are optional additional local variables.
168
-
169
- Returns the result of the evaluation.
170
- """
171
- f_locals = self.f_locals.copy()
172
- f_locals.update(vars)
173
- return eval(code, self.f_globals, f_locals)
174
-
175
- def repr(self, object: object) -> str:
176
- """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""
177
- return saferepr(object)
178
-
179
- def getargs(self, var: bool = False):
180
- """Return a list of tuples (name, value) for all arguments.
181
-
182
- If 'var' is set True, also include the variable and keyword arguments
183
- when present.
184
- """
185
- retval = []
186
- for arg in self.code.getargs(var):
187
- try:
188
- retval.append((arg, self.f_locals[arg]))
189
- except KeyError:
190
- pass # this can occur when using Psyco
191
- return retval
192
-
193
-
194
- class TracebackEntry:
195
- """A single entry in a Traceback."""
196
-
197
- __slots__ = ("_rawentry", "_repr_style")
198
-
199
- def __init__(
200
- self,
201
- rawentry: TracebackType,
202
- repr_style: Literal["short", "long"] | None = None,
203
- ) -> None:
204
- self._rawentry: Final = rawentry
205
- self._repr_style: Final = repr_style
206
-
207
- def with_repr_style(
208
- self, repr_style: Literal["short", "long"] | None
209
- ) -> TracebackEntry:
210
- return TracebackEntry(self._rawentry, repr_style)
211
-
212
- @property
213
- def lineno(self) -> int:
214
- return self._rawentry.tb_lineno - 1
215
-
216
- def get_python_framesummary(self) -> FrameSummary:
217
- # Python's built-in traceback module implements all the nitty gritty
218
- # details to get column numbers of out frames.
219
- stack_summary = extract_tb(self._rawentry, limit=1)
220
- return stack_summary[0]
221
-
222
- # Column and end line numbers introduced in python 3.11
223
- if sys.version_info < (3, 11):
224
-
225
- @property
226
- def end_lineno_relative(self) -> int | None:
227
- return None
228
-
229
- @property
230
- def colno(self) -> int | None:
231
- return None
232
-
233
- @property
234
- def end_colno(self) -> int | None:
235
- return None
236
- else:
237
-
238
- @property
239
- def end_lineno_relative(self) -> int | None:
240
- frame_summary = self.get_python_framesummary()
241
- if frame_summary.end_lineno is None: # pragma: no cover
242
- return None
243
- return frame_summary.end_lineno - 1 - self.frame.code.firstlineno
244
-
245
- @property
246
- def colno(self) -> int | None:
247
- """Starting byte offset of the expression in the traceback entry."""
248
- return self.get_python_framesummary().colno
249
-
250
- @property
251
- def end_colno(self) -> int | None:
252
- """Ending byte offset of the expression in the traceback entry."""
253
- return self.get_python_framesummary().end_colno
254
-
255
- @property
256
- def frame(self) -> Frame:
257
- return Frame(self._rawentry.tb_frame)
258
-
259
- @property
260
- def relline(self) -> int:
261
- return self.lineno - self.frame.code.firstlineno
262
-
263
- def __repr__(self) -> str:
264
- return f"<TracebackEntry {self.frame.code.path}:{self.lineno + 1}>"
265
-
266
- @property
267
- def statement(self) -> Source:
268
- """_pytest._code.Source object for the current statement."""
269
- source = self.frame.code.fullsource
270
- assert source is not None
271
- return source.getstatement(self.lineno)
272
-
273
- @property
274
- def path(self) -> Path | str:
275
- """Path to the source code."""
276
- return self.frame.code.path
277
-
278
- @property
279
- def locals(self) -> dict[str, Any]:
280
- """Locals of underlying frame."""
281
- return self.frame.f_locals
282
-
283
- def getfirstlinesource(self) -> int:
284
- return self.frame.code.firstlineno
285
-
286
- def getsource(
287
- self, astcache: dict[str | Path, ast.AST] | None = None
288
- ) -> Source | None:
289
- """Return failing source code."""
290
- # we use the passed in astcache to not reparse asttrees
291
- # within exception info printing
292
- source = self.frame.code.fullsource
293
- if source is None:
294
- return None
295
- key = astnode = None
296
- if astcache is not None:
297
- key = self.frame.code.path
298
- if key is not None:
299
- astnode = astcache.get(key, None)
300
- start = self.getfirstlinesource()
301
- try:
302
- astnode, _, end = getstatementrange_ast(
303
- self.lineno, source, astnode=astnode
304
- )
305
- except SyntaxError:
306
- end = self.lineno + 1
307
- else:
308
- if key is not None and astcache is not None:
309
- astcache[key] = astnode
310
- return source[start:end]
311
-
312
- source = property(getsource)
313
-
314
- def ishidden(self, excinfo: ExceptionInfo[BaseException] | None) -> bool:
315
- """Return True if the current frame has a var __tracebackhide__
316
- resolving to True.
317
-
318
- If __tracebackhide__ is a callable, it gets called with the
319
- ExceptionInfo instance and can decide whether to hide the traceback.
320
-
321
- Mostly for internal use.
322
- """
323
- tbh: bool | Callable[[ExceptionInfo[BaseException] | None], bool] = False
324
- for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
325
- # in normal cases, f_locals and f_globals are dictionaries
326
- # however via `exec(...)` / `eval(...)` they can be other types
327
- # (even incorrect types!).
328
- # as such, we suppress all exceptions while accessing __tracebackhide__
329
- try:
330
- tbh = maybe_ns_dct["__tracebackhide__"]
331
- except Exception:
332
- pass
333
- else:
334
- break
335
- if tbh and callable(tbh):
336
- return tbh(excinfo)
337
- return tbh
338
-
339
- def __str__(self) -> str:
340
- name = self.frame.code.name
341
- try:
342
- line = str(self.statement).lstrip()
343
- except KeyboardInterrupt:
344
- raise
345
- except BaseException:
346
- line = "???"
347
- # This output does not quite match Python's repr for traceback entries,
348
- # but changing it to do so would break certain plugins. See
349
- # https://github.com/pytest-dev/pytest/pull/7535/ for details.
350
- return f" File '{self.path}':{self.lineno + 1} in {name}\n {line}\n"
351
-
352
- @property
353
- def name(self) -> str:
354
- """co_name of underlying code."""
355
- return self.frame.code.raw.co_name
356
-
357
-
358
- class Traceback(list[TracebackEntry]):
359
- """Traceback objects encapsulate and offer higher level access to Traceback entries."""
360
-
361
- def __init__(
362
- self,
363
- tb: TracebackType | Iterable[TracebackEntry],
364
- ) -> None:
365
- """Initialize from given python traceback object and ExceptionInfo."""
366
- if isinstance(tb, TracebackType):
367
-
368
- def f(cur: TracebackType) -> Iterable[TracebackEntry]:
369
- cur_: TracebackType | None = cur
370
- while cur_ is not None:
371
- yield TracebackEntry(cur_)
372
- cur_ = cur_.tb_next
373
-
374
- super().__init__(f(tb))
375
- else:
376
- super().__init__(tb)
377
-
378
- def cut(
379
- self,
380
- path: os.PathLike[str] | str | None = None,
381
- lineno: int | None = None,
382
- firstlineno: int | None = None,
383
- excludepath: os.PathLike[str] | None = None,
384
- ) -> Traceback:
385
- """Return a Traceback instance wrapping part of this Traceback.
386
-
387
- By providing any combination of path, lineno and firstlineno, the
388
- first frame to start the to-be-returned traceback is determined.
389
-
390
- This allows cutting the first part of a Traceback instance e.g.
391
- for formatting reasons (removing some uninteresting bits that deal
392
- with handling of the exception/traceback).
393
- """
394
- path_ = None if path is None else os.fspath(path)
395
- excludepath_ = None if excludepath is None else os.fspath(excludepath)
396
- for x in self:
397
- code = x.frame.code
398
- codepath = code.path
399
- if path is not None and str(codepath) != path_:
400
- continue
401
- if (
402
- excludepath is not None
403
- and isinstance(codepath, Path)
404
- and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
405
- ):
406
- continue
407
- if lineno is not None and x.lineno != lineno:
408
- continue
409
- if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
410
- continue
411
- return Traceback(x._rawentry)
412
- return self
413
-
414
- @overload
415
- def __getitem__(self, key: SupportsIndex) -> TracebackEntry: ...
416
-
417
- @overload
418
- def __getitem__(self, key: slice) -> Traceback: ...
419
-
420
- def __getitem__(self, key: SupportsIndex | slice) -> TracebackEntry | Traceback:
421
- if isinstance(key, slice):
422
- return self.__class__(super().__getitem__(key))
423
- else:
424
- return super().__getitem__(key)
425
-
426
- def filter(
427
- self,
428
- excinfo_or_fn: ExceptionInfo[BaseException] | Callable[[TracebackEntry], bool],
429
- /,
430
- ) -> Traceback:
431
- """Return a Traceback instance with certain items removed.
432
-
433
- If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
434
- which are hidden (see ishidden() above).
435
-
436
- Otherwise, the filter is a function that gets a single argument, a
437
- ``TracebackEntry`` instance, and should return True when the item should
438
- be added to the ``Traceback``, False when not.
439
- """
440
- if isinstance(excinfo_or_fn, ExceptionInfo):
441
- fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731
442
- else:
443
- fn = excinfo_or_fn
444
- return Traceback(filter(fn, self))
445
-
446
- def recursionindex(self) -> int | None:
447
- """Return the index of the frame/TracebackEntry where recursion originates if
448
- appropriate, None if no recursion occurred."""
449
- cache: dict[tuple[Any, int, int], list[dict[str, Any]]] = {}
450
- for i, entry in enumerate(self):
451
- # id for the code.raw is needed to work around
452
- # the strange metaprogramming in the decorator lib from pypi
453
- # which generates code objects that have hash/value equality
454
- # XXX needs a test
455
- key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
456
- values = cache.setdefault(key, [])
457
- # Since Python 3.13 f_locals is a proxy, freeze it.
458
- loc = dict(entry.frame.f_locals)
459
- if values:
460
- for otherloc in values:
461
- if otherloc == loc:
462
- return i
463
- values.append(loc)
464
- return None
465
-
466
-
467
- def stringify_exception(
468
- exc: BaseException, include_subexception_msg: bool = True
469
- ) -> str:
470
- try:
471
- notes = getattr(exc, "__notes__", [])
472
- except KeyError:
473
- # Workaround for https://github.com/python/cpython/issues/98778 on
474
- # some 3.10 and 3.11 patch versions.
475
- HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
476
- if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
477
- notes = []
478
- else: # pragma: no cover
479
- # exception not related to above bug, reraise
480
- raise
481
- if not include_subexception_msg and isinstance(exc, BaseExceptionGroup):
482
- message = exc.message
483
- else:
484
- message = str(exc)
485
-
486
- return "\n".join(
487
- [
488
- message,
489
- *notes,
490
- ]
491
- )
492
-
493
-
494
- E = TypeVar("E", bound=BaseException, covariant=True)
495
-
496
-
497
- @final
498
- @dataclasses.dataclass
499
- class ExceptionInfo(Generic[E]):
500
- """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
501
-
502
- _assert_start_repr: ClassVar = "AssertionError('assert "
503
-
504
- _excinfo: tuple[type[E], E, TracebackType] | None
505
- _striptext: str
506
- _traceback: Traceback | None
507
-
508
- def __init__(
509
- self,
510
- excinfo: tuple[type[E], E, TracebackType] | None,
511
- striptext: str = "",
512
- traceback: Traceback | None = None,
513
- *,
514
- _ispytest: bool = False,
515
- ) -> None:
516
- check_ispytest(_ispytest)
517
- self._excinfo = excinfo
518
- self._striptext = striptext
519
- self._traceback = traceback
520
-
521
- @classmethod
522
- def from_exception(
523
- cls,
524
- # Ignoring error: "Cannot use a covariant type variable as a parameter".
525
- # This is OK to ignore because this class is (conceptually) readonly.
526
- # See https://github.com/python/mypy/issues/7049.
527
- exception: E, # type: ignore[misc]
528
- exprinfo: str | None = None,
529
- ) -> ExceptionInfo[E]:
530
- """Return an ExceptionInfo for an existing exception.
531
-
532
- The exception must have a non-``None`` ``__traceback__`` attribute,
533
- otherwise this function fails with an assertion error. This means that
534
- the exception must have been raised, or added a traceback with the
535
- :py:meth:`~BaseException.with_traceback()` method.
536
-
537
- :param exprinfo:
538
- A text string helping to determine if we should strip
539
- ``AssertionError`` from the output. Defaults to the exception
540
- message/``__str__()``.
541
-
542
- .. versionadded:: 7.4
543
- """
544
- assert exception.__traceback__, (
545
- "Exceptions passed to ExcInfo.from_exception(...)"
546
- " must have a non-None __traceback__."
547
- )
548
- exc_info = (type(exception), exception, exception.__traceback__)
549
- return cls.from_exc_info(exc_info, exprinfo)
550
-
551
- @classmethod
552
- def from_exc_info(
553
- cls,
554
- exc_info: tuple[type[E], E, TracebackType],
555
- exprinfo: str | None = None,
556
- ) -> ExceptionInfo[E]:
557
- """Like :func:`from_exception`, but using old-style exc_info tuple."""
558
- _striptext = ""
559
- if exprinfo is None and isinstance(exc_info[1], AssertionError):
560
- exprinfo = getattr(exc_info[1], "msg", None)
561
- if exprinfo is None:
562
- exprinfo = saferepr(exc_info[1])
563
- if exprinfo and exprinfo.startswith(cls._assert_start_repr):
564
- _striptext = "AssertionError: "
565
-
566
- return cls(exc_info, _striptext, _ispytest=True)
567
-
568
- @classmethod
569
- def from_current(cls, exprinfo: str | None = None) -> ExceptionInfo[BaseException]:
570
- """Return an ExceptionInfo matching the current traceback.
571
-
572
- .. warning::
573
-
574
- Experimental API
575
-
576
- :param exprinfo:
577
- A text string helping to determine if we should strip
578
- ``AssertionError`` from the output. Defaults to the exception
579
- message/``__str__()``.
580
- """
581
- tup = sys.exc_info()
582
- assert tup[0] is not None, "no current exception"
583
- assert tup[1] is not None, "no current exception"
584
- assert tup[2] is not None, "no current exception"
585
- exc_info = (tup[0], tup[1], tup[2])
586
- return ExceptionInfo.from_exc_info(exc_info, exprinfo)
587
-
588
- @classmethod
589
- def for_later(cls) -> ExceptionInfo[E]:
590
- """Return an unfilled ExceptionInfo."""
591
- return cls(None, _ispytest=True)
592
-
593
- def fill_unfilled(self, exc_info: tuple[type[E], E, TracebackType]) -> None:
594
- """Fill an unfilled ExceptionInfo created with ``for_later()``."""
595
- assert self._excinfo is None, "ExceptionInfo was already filled"
596
- self._excinfo = exc_info
597
-
598
- @property
599
- def type(self) -> type[E]:
600
- """The exception class."""
601
- assert self._excinfo is not None, (
602
- ".type can only be used after the context manager exits"
603
- )
604
- return self._excinfo[0]
605
-
606
- @property
607
- def value(self) -> E:
608
- """The exception value."""
609
- assert self._excinfo is not None, (
610
- ".value can only be used after the context manager exits"
611
- )
612
- return self._excinfo[1]
613
-
614
- @property
615
- def tb(self) -> TracebackType:
616
- """The exception raw traceback."""
617
- assert self._excinfo is not None, (
618
- ".tb can only be used after the context manager exits"
619
- )
620
- return self._excinfo[2]
621
-
622
- @property
623
- def typename(self) -> str:
624
- """The type name of the exception."""
625
- assert self._excinfo is not None, (
626
- ".typename can only be used after the context manager exits"
627
- )
628
- return self.type.__name__
629
-
630
- @property
631
- def traceback(self) -> Traceback:
632
- """The traceback."""
633
- if self._traceback is None:
634
- self._traceback = Traceback(self.tb)
635
- return self._traceback
636
-
637
- @traceback.setter
638
- def traceback(self, value: Traceback) -> None:
639
- self._traceback = value
640
-
641
- def __repr__(self) -> str:
642
- if self._excinfo is None:
643
- return "<ExceptionInfo for raises contextmanager>"
644
- return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
645
-
646
- def exconly(self, tryshort: bool = False) -> str:
647
- """Return the exception as a string.
648
-
649
- This is usually a single line "<exception type>: <exception str>", but
650
- may also include additional lines for the exception notes, and detailed
651
- information for SyntaxError's.
652
-
653
- :param tryshort:
654
- If true, and the exception is an AssertionError, strip
655
- 'AssertionError: ' from the beginning.
656
- """
657
-
658
- def _get_single_subexc(
659
- eg: BaseExceptionGroup[BaseException],
660
- ) -> BaseException | None:
661
- if len(eg.exceptions) != 1:
662
- return None
663
- if isinstance(e := eg.exceptions[0], BaseExceptionGroup):
664
- return _get_single_subexc(e)
665
- return e
666
-
667
- if (
668
- tryshort
669
- and isinstance(self.value, BaseExceptionGroup)
670
- and (subexc := _get_single_subexc(self.value)) is not None
671
- ):
672
- return f"{subexc!r} [single exception in {type(self.value).__name__}]"
673
-
674
- lines = format_exception_only(self.value)
675
- # The lines already include line separators.
676
- text = "".join(lines)
677
- text = text.rstrip()
678
- if tryshort:
679
- if text.startswith(self._striptext):
680
- text = text[len(self._striptext) :]
681
- return text
682
-
683
- def errisinstance(self, exc: EXCEPTION_OR_MORE) -> bool:
684
- """Return True if the exception is an instance of exc.
685
-
686
- Consider using ``isinstance(excinfo.value, exc)`` instead.
687
- """
688
- return isinstance(self.value, exc)
689
-
690
- def _getreprcrash(self) -> ReprFileLocation | None:
691
- # Find last non-hidden traceback entry that led to the exception of the
692
- # traceback, or None if all hidden.
693
- for i in range(-1, -len(self.traceback) - 1, -1):
694
- entry = self.traceback[i]
695
- if not entry.ishidden(self):
696
- path, lineno = entry.frame.code.raw.co_filename, entry.lineno
697
- exconly = self.exconly(tryshort=True)
698
- return ReprFileLocation(path, lineno + 1, exconly)
699
- return None
700
-
701
- def getrepr(
702
- self,
703
- showlocals: bool = False,
704
- style: TracebackStyle = "long",
705
- abspath: bool = False,
706
- tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True,
707
- funcargs: bool = False,
708
- truncate_locals: bool = True,
709
- truncate_args: bool = True,
710
- chain: bool = True,
711
- ) -> ReprExceptionInfo | ExceptionChainRepr:
712
- """Return str()able representation of this exception info.
713
-
714
- The formatting parameters are ineffective if ``style="native"``,
715
- since in this case the native formatting is used.
716
-
717
- :param bool showlocals:
718
- Show locals per traceback entry.
719
-
720
- :param str style:
721
- long|short|line|no|native|value traceback style.
722
-
723
- :param bool abspath:
724
- If paths should be changed to absolute or left unchanged.
725
-
726
- :param tbfilter:
727
- A filter for traceback entries.
728
-
729
- * If false, don't hide any entries.
730
- * If true, hide internal entries and entries that contain a local
731
- variable ``__tracebackhide__ = True``.
732
- * If a callable, delegates the filtering to the callable.
733
-
734
- :param bool funcargs:
735
- Show function arguments per traceback entry.
736
-
737
- :param bool truncate_locals:
738
- Whether to show a size-limited `repr()` of locals, or a full
739
- pretty-printing.
740
-
741
- :param bool truncate_args:
742
- Whether to show a size-limited truncated `repr()` of function
743
- arguments, or a full pretty-printing.
744
-
745
- :param bool chain:
746
- If chained exceptions should be shown.
747
-
748
- .. versionchanged:: 3.9
749
-
750
- Added the ``chain`` parameter.
751
- """
752
- if style == "native":
753
- return ReprExceptionInfo(
754
- reprtraceback=ReprTracebackNative(
755
- format_exception(
756
- self.type,
757
- self.value,
758
- self.traceback[0]._rawentry if self.traceback else None,
759
- )
760
- ),
761
- reprcrash=self._getreprcrash(),
762
- )
763
-
764
- fmt = ExceptionInfoFormatter(
765
- showlocals=showlocals,
766
- style=style,
767
- abspath=abspath,
768
- tbfilter=tbfilter,
769
- funcargs=funcargs,
770
- truncate_locals=truncate_locals,
771
- truncate_args=truncate_args,
772
- chain=chain,
773
- )
774
- return fmt.repr_excinfo(self)
775
-
776
- def match(self, regexp: str | re.Pattern[str]) -> Literal[True]:
777
- """Check whether the regular expression `regexp` matches the string
778
- representation of the exception using :func:`python:re.search`.
779
-
780
- If it matches `True` is returned, otherwise an `AssertionError` is raised.
781
- """
782
- __tracebackhide__ = True
783
- value = stringify_exception(self.value)
784
- msg = (
785
- f"Regex pattern did not match.\n"
786
- f" Expected regex: {regexp!r}\n"
787
- f" Actual message: {value!r}"
788
- )
789
- if regexp == value:
790
- msg += "\n Did you mean to `re.escape()` the regex?"
791
- assert re.search(regexp, value), msg
792
- # Return True to allow for "assert excinfo.match()".
793
- return True
794
-
795
- def _group_contains(
796
- self,
797
- exc_group: BaseExceptionGroup[BaseException],
798
- expected_exception: EXCEPTION_OR_MORE,
799
- match: str | re.Pattern[str] | None,
800
- target_depth: int | None = None,
801
- current_depth: int = 1,
802
- ) -> bool:
803
- """Return `True` if a `BaseExceptionGroup` contains a matching exception."""
804
- if (target_depth is not None) and (current_depth > target_depth):
805
- # already descended past the target depth
806
- return False
807
- for exc in exc_group.exceptions:
808
- if isinstance(exc, BaseExceptionGroup):
809
- if self._group_contains(
810
- exc, expected_exception, match, target_depth, current_depth + 1
811
- ):
812
- return True
813
- if (target_depth is not None) and (current_depth != target_depth):
814
- # not at the target depth, no match
815
- continue
816
- if not isinstance(exc, expected_exception):
817
- continue
818
- if match is not None:
819
- value = stringify_exception(exc)
820
- if not re.search(match, value):
821
- continue
822
- return True
823
- return False
824
-
825
- def group_contains(
826
- self,
827
- expected_exception: EXCEPTION_OR_MORE,
828
- *,
829
- match: str | re.Pattern[str] | None = None,
830
- depth: int | None = None,
831
- ) -> bool:
832
- """Check whether a captured exception group contains a matching exception.
833
-
834
- :param Type[BaseException] | Tuple[Type[BaseException]] expected_exception:
835
- The expected exception type, or a tuple if one of multiple possible
836
- exception types are expected.
837
-
838
- :param str | re.Pattern[str] | None match:
839
- If specified, a string containing a regular expression,
840
- or a regular expression object, that is tested against the string
841
- representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__`
842
- using :func:`re.search`.
843
-
844
- To match a literal string that may contain :ref:`special characters
845
- <re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
846
-
847
- :param Optional[int] depth:
848
- If `None`, will search for a matching exception at any nesting depth.
849
- If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
850
- the exceptions contained within the topmost exception group).
851
-
852
- .. versionadded:: 8.0
853
-
854
- .. warning::
855
- This helper makes it easy to check for the presence of specific exceptions,
856
- but it is very bad for checking that the group does *not* contain
857
- *any other exceptions*.
858
- You should instead consider using :class:`pytest.RaisesGroup`
859
-
860
- """
861
- msg = "Captured exception is not an instance of `BaseExceptionGroup`"
862
- assert isinstance(self.value, BaseExceptionGroup), msg
863
- msg = "`depth` must be >= 1 if specified"
864
- assert (depth is None) or (depth >= 1), msg
865
- return self._group_contains(self.value, expected_exception, match, depth)
866
-
867
-
868
- # Type alias for the `tbfilter` setting:
869
- # bool: If True, it should be filtered using Traceback.filter()
870
- # callable: A callable that takes an ExceptionInfo and returns the filtered traceback.
871
- TracebackFilter: TypeAlias = bool | Callable[[ExceptionInfo[BaseException]], Traceback]
872
-
873
-
874
- @dataclasses.dataclass
875
- class ExceptionInfoFormatter:
876
- """Helper object to format ExceptionInfo's and individual exception parts
877
- into TerminalRepr's.
878
-
879
- See :func:`ExceptionInfo.getrepr` for parameters.
880
- """
881
-
882
- # for traceback entries
883
- flow_marker: ClassVar = ">"
884
- fail_marker: ClassVar = "E"
885
-
886
- showlocals: bool = False
887
- # Note: "native" is handled outside of ExceptionInfoFormatter.
888
- style: TracebackStyle = "long"
889
- abspath: bool = True
890
- tbfilter: TracebackFilter = True
891
- funcargs: bool = False
892
- truncate_locals: bool = True
893
- truncate_args: bool = True
894
- chain: bool = True
895
-
896
- astcache: dict[str | Path, ast.AST] = dataclasses.field(
897
- default_factory=dict, init=False, repr=False
898
- )
899
-
900
- def _getindent(self, source: Source) -> int:
901
- # Figure out indent for the given source.
902
- try:
903
- s = str(source.getstatement(len(source) - 1))
904
- except KeyboardInterrupt:
905
- raise
906
- except BaseException:
907
- try:
908
- s = str(source[-1])
909
- except KeyboardInterrupt:
910
- raise
911
- except BaseException:
912
- return 0
913
- return 4 + (len(s) - len(s.lstrip()))
914
-
915
- def _getentrysource(self, entry: TracebackEntry) -> Source | None:
916
- source = entry.getsource(self.astcache)
917
- if source is not None:
918
- source = source.deindent()
919
- return source
920
-
921
- def repr_args(self, entry: TracebackEntry) -> ReprFuncArgs | None:
922
- if self.funcargs:
923
- args = []
924
- for argname, argvalue in entry.frame.getargs(var=True):
925
- if self.truncate_args:
926
- str_repr = saferepr(argvalue)
927
- else:
928
- str_repr = saferepr(argvalue, maxsize=None)
929
- args.append((argname, str_repr))
930
- return ReprFuncArgs(args)
931
- return None
932
-
933
- def get_source(
934
- self,
935
- source: Source | None,
936
- line_index: int = -1,
937
- excinfo: ExceptionInfo[BaseException] | None = None,
938
- short: bool = False,
939
- end_line_index: int | None = None,
940
- colno: int | None = None,
941
- end_colno: int | None = None,
942
- ) -> list[str]:
943
- """Return formatted and marked up source lines."""
944
- lines = []
945
- if source is not None and line_index < 0:
946
- line_index += len(source)
947
- if source is None or line_index >= len(source.lines) or line_index < 0:
948
- # `line_index` could still be outside `range(len(source.lines))` if
949
- # we're processing AST with pathological position attributes.
950
- source = Source("???")
951
- line_index = 0
952
- space_prefix = " "
953
- if short:
954
- lines.append(space_prefix + source.lines[line_index].strip())
955
- lines.extend(
956
- self.get_highlight_arrows_for_line(
957
- raw_line=source.raw_lines[line_index],
958
- line=source.lines[line_index].strip(),
959
- lineno=line_index,
960
- end_lineno=end_line_index,
961
- colno=colno,
962
- end_colno=end_colno,
963
- )
964
- )
965
- else:
966
- for line in source.lines[:line_index]:
967
- lines.append(space_prefix + line)
968
- lines.append(self.flow_marker + " " + source.lines[line_index])
969
- lines.extend(
970
- self.get_highlight_arrows_for_line(
971
- raw_line=source.raw_lines[line_index],
972
- line=source.lines[line_index],
973
- lineno=line_index,
974
- end_lineno=end_line_index,
975
- colno=colno,
976
- end_colno=end_colno,
977
- )
978
- )
979
- for line in source.lines[line_index + 1 :]:
980
- lines.append(space_prefix + line)
981
- if excinfo is not None:
982
- indent = 4 if short else self._getindent(source)
983
- lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
984
- return lines
985
-
986
- def get_highlight_arrows_for_line(
987
- self,
988
- line: str,
989
- raw_line: str,
990
- lineno: int | None,
991
- end_lineno: int | None,
992
- colno: int | None,
993
- end_colno: int | None,
994
- ) -> list[str]:
995
- """Return characters highlighting a source line.
996
-
997
- Example with colno and end_colno pointing to the bar expression:
998
- "foo() + bar()"
999
- returns " ^^^^^"
1000
- """
1001
- if lineno != end_lineno:
1002
- # Don't handle expressions that span multiple lines.
1003
- return []
1004
- if colno is None or end_colno is None:
1005
- # Can't do anything without column information.
1006
- return []
1007
-
1008
- num_stripped_chars = len(raw_line) - len(line)
1009
-
1010
- start_char_offset = _byte_offset_to_character_offset(raw_line, colno)
1011
- end_char_offset = _byte_offset_to_character_offset(raw_line, end_colno)
1012
- num_carets = end_char_offset - start_char_offset
1013
- # If the highlight would span the whole line, it is redundant, don't
1014
- # show it.
1015
- if num_carets >= len(line.strip()):
1016
- return []
1017
-
1018
- highlights = " "
1019
- highlights += " " * (start_char_offset - num_stripped_chars + 1)
1020
- highlights += "^" * num_carets
1021
- return [highlights]
1022
-
1023
- def get_exconly(
1024
- self,
1025
- excinfo: ExceptionInfo[BaseException],
1026
- indent: int = 4,
1027
- markall: bool = False,
1028
- ) -> list[str]:
1029
- lines = []
1030
- indentstr = " " * indent
1031
- # Get the real exception information out.
1032
- exlines = excinfo.exconly(tryshort=True).split("\n")
1033
- failindent = self.fail_marker + indentstr[1:]
1034
- for line in exlines:
1035
- lines.append(failindent + line)
1036
- if not markall:
1037
- failindent = indentstr
1038
- return lines
1039
-
1040
- def repr_locals(self, locals: Mapping[str, object]) -> ReprLocals | None:
1041
- if self.showlocals:
1042
- lines = []
1043
- # Variables starting with `@` are helpers injected by assertion
1044
- # rewriting, not user variables, so hide them.
1045
- keys = [loc for loc in locals if loc[0] != "@"]
1046
- keys.sort()
1047
- for name in keys:
1048
- value = locals[name]
1049
- if name == "__builtins__":
1050
- lines.append("__builtins__ = <builtins>")
1051
- else:
1052
- # This formatting could all be handled by the
1053
- # _repr() function, which is only reprlib.Repr in
1054
- # disguise, so is very configurable.
1055
- if self.truncate_locals:
1056
- str_repr = saferepr(value)
1057
- else:
1058
- str_repr = safeformat(value)
1059
- # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
1060
- lines.append(f"{name:<10} = {str_repr}")
1061
- # else:
1062
- # self._line("%-10s =\\" % (name,))
1063
- # # XXX
1064
- # pprint.pprint(value, stream=self.excinfowriter)
1065
- return ReprLocals(lines)
1066
- return None
1067
-
1068
- def repr_traceback_entry(
1069
- self,
1070
- entry: TracebackEntry | None,
1071
- excinfo: ExceptionInfo[BaseException] | None = None,
1072
- ) -> ReprEntry:
1073
- lines: list[str] = []
1074
- style = (
1075
- entry._repr_style
1076
- if entry is not None and entry._repr_style is not None
1077
- else self.style
1078
- )
1079
- if style in ("short", "long") and entry is not None:
1080
- source = self._getentrysource(entry)
1081
- if source is None:
1082
- source = Source("???")
1083
- line_index = 0
1084
- end_line_index, colno, end_colno = None, None, None
1085
- else:
1086
- line_index = entry.relline
1087
- end_line_index = entry.end_lineno_relative
1088
- colno = entry.colno
1089
- end_colno = entry.end_colno
1090
- short = style == "short"
1091
- reprargs = self.repr_args(entry) if not short else None
1092
- s = self.get_source(
1093
- source=source,
1094
- line_index=line_index,
1095
- excinfo=excinfo,
1096
- short=short,
1097
- end_line_index=end_line_index,
1098
- colno=colno,
1099
- end_colno=end_colno,
1100
- )
1101
- lines.extend(s)
1102
- if short:
1103
- message = f"in {entry.name}"
1104
- else:
1105
- message = (excinfo and excinfo.typename) or ""
1106
- entry_path = entry.path
1107
- path = self._makepath(entry_path)
1108
- reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
1109
- localsrepr = self.repr_locals(entry.locals)
1110
- return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
1111
- elif style == "value":
1112
- if excinfo:
1113
- lines.extend(str(excinfo.value).split("\n"))
1114
- return ReprEntry(lines, None, None, None, style)
1115
- else:
1116
- if excinfo:
1117
- lines.extend(self.get_exconly(excinfo, indent=4))
1118
- return ReprEntry(lines, None, None, None, style)
1119
-
1120
- def _makepath(self, path: Path | str) -> str:
1121
- if not self.abspath and isinstance(path, Path):
1122
- try:
1123
- np = bestrelpath(Path.cwd(), path)
1124
- except OSError:
1125
- return str(path)
1126
- if len(np) < len(str(path)):
1127
- return np
1128
- return str(path)
1129
-
1130
- def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback:
1131
- traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
1132
-
1133
- if isinstance(excinfo.value, RecursionError):
1134
- traceback, extraline = self._truncate_recursive_traceback(traceback)
1135
- else:
1136
- extraline = None
1137
-
1138
- if not traceback:
1139
- if extraline is None:
1140
- extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
1141
- entries = [self.repr_traceback_entry(None, excinfo)]
1142
- return ReprTraceback(entries, extraline, style=self.style)
1143
-
1144
- last = traceback[-1]
1145
- if self.style == "value":
1146
- entries = [self.repr_traceback_entry(last, excinfo)]
1147
- return ReprTraceback(entries, None, style=self.style)
1148
-
1149
- entries = [
1150
- self.repr_traceback_entry(entry, excinfo if last == entry else None)
1151
- for entry in traceback
1152
- ]
1153
- return ReprTraceback(entries, extraline, style=self.style)
1154
-
1155
- def _truncate_recursive_traceback(
1156
- self, traceback: Traceback
1157
- ) -> tuple[Traceback, str | None]:
1158
- """Truncate the given recursive traceback trying to find the starting
1159
- point of the recursion.
1160
-
1161
- The detection is done by going through each traceback entry and
1162
- finding the point in which the locals of the frame are equal to the
1163
- locals of a previous frame (see ``recursionindex()``).
1164
-
1165
- Handle the situation where the recursion process might raise an
1166
- exception (for example comparing numpy arrays using equality raises a
1167
- TypeError), in which case we do our best to warn the user of the
1168
- error and show a limited traceback.
1169
- """
1170
- try:
1171
- recursionindex = traceback.recursionindex()
1172
- except Exception as e:
1173
- max_frames = 10
1174
- extraline: str | None = (
1175
- "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
1176
- " The following exception happened when comparing locals in the stack frame:\n"
1177
- f" {type(e).__name__}: {e!s}\n"
1178
- f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
1179
- )
1180
- # Type ignored because adding two instances of a List subtype
1181
- # currently incorrectly has type List instead of the subtype.
1182
- traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
1183
- else:
1184
- if recursionindex is not None:
1185
- extraline = "!!! Recursion detected (same locals & position)"
1186
- traceback = traceback[: recursionindex + 1]
1187
- else:
1188
- extraline = None
1189
-
1190
- return traceback, extraline
1191
-
1192
- def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainRepr:
1193
- repr_chain: list[tuple[ReprTraceback, ReprFileLocation | None, str | None]] = []
1194
- e: BaseException | None = excinfo.value
1195
- excinfo_: ExceptionInfo[BaseException] | None = excinfo
1196
- description = None
1197
- seen: set[int] = set()
1198
- while e is not None and id(e) not in seen:
1199
- seen.add(id(e))
1200
-
1201
- if excinfo_:
1202
- # Fall back to native traceback as a temporary workaround until
1203
- # full support for exception groups added to ExceptionInfo.
1204
- # See https://github.com/pytest-dev/pytest/issues/9159
1205
- reprtraceback: ReprTraceback | ReprTracebackNative
1206
- if isinstance(e, BaseExceptionGroup):
1207
- # don't filter any sub-exceptions since they shouldn't have any internal frames
1208
- traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
1209
- extraline = (
1210
- "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
1211
- if not traceback
1212
- else None
1213
- )
1214
- reprtraceback = ReprTracebackNative(
1215
- format_exception(
1216
- type(excinfo.value),
1217
- excinfo.value,
1218
- traceback[0]._rawentry if traceback else None,
1219
- ),
1220
- extraline=extraline,
1221
- )
1222
-
1223
- else:
1224
- reprtraceback = self.repr_traceback(excinfo_)
1225
- reprcrash = excinfo_._getreprcrash()
1226
- else:
1227
- # Fallback to native repr if the exception doesn't have a traceback:
1228
- # ExceptionInfo objects require a full traceback to work.
1229
- reprtraceback = ReprTracebackNative(format_exception(type(e), e, None))
1230
- reprcrash = None
1231
- repr_chain.append((reprtraceback, reprcrash, description))
1232
-
1233
- if e.__cause__ is not None and self.chain:
1234
- e = e.__cause__
1235
- excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
1236
- description = "The above exception was the direct cause of the following exception:"
1237
- elif (
1238
- e.__context__ is not None and not e.__suppress_context__ and self.chain
1239
- ):
1240
- e = e.__context__
1241
- excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
1242
- description = "During handling of the above exception, another exception occurred:"
1243
- else:
1244
- e = None
1245
- repr_chain.reverse()
1246
- return ExceptionChainRepr(repr_chain)
1247
-
1248
-
1249
- @dataclasses.dataclass(eq=False)
1250
- class TerminalRepr:
1251
- """Base class for terminal representations -- pieces of data that display
1252
- themselves to a terminal."""
1253
-
1254
- def __str__(self) -> str:
1255
- # FYI this is called from pytest-xdist's serialization of exception
1256
- # information.
1257
- io = StringIO()
1258
- tw = TerminalWriter(file=io)
1259
- self.toterminal(tw)
1260
- return io.getvalue().strip()
1261
-
1262
- def __repr__(self) -> str:
1263
- return f"<{self.__class__} instance at {id(self):0x}>"
1264
-
1265
- def toterminal(self, tw: TerminalWriter) -> None:
1266
- raise NotImplementedError()
1267
-
1268
-
1269
- @dataclasses.dataclass(eq=False)
1270
- class ExceptionRepr(TerminalRepr):
1271
- """Base class for exception terminal representations.
1272
-
1273
- The representation generally contains:
1274
- - The exception traceback (`reprtraceback`)
1275
- - The exception message and location (`reprcrash`)
1276
- - Separated, titled sections with additional data (pytest core doesn't use
1277
- this currently).
1278
- """
1279
-
1280
- reprtraceback: ReprTraceback
1281
- reprcrash: ReprFileLocation | None
1282
- sections: list[tuple[str, str, str]] = dataclasses.field(
1283
- init=False, default_factory=list
1284
- )
1285
-
1286
- def addsection(self, name: str, content: str, sep: str = "-") -> None:
1287
- self.sections.append((name, content, sep))
1288
-
1289
- def toterminal(self, tw: TerminalWriter) -> None:
1290
- for name, content, sep in self.sections:
1291
- tw.sep(sep, name)
1292
- tw.line(content)
1293
-
1294
-
1295
- @dataclasses.dataclass(eq=False)
1296
- class ExceptionChainRepr(ExceptionRepr):
1297
- """A chain of exceptions, separated by descriptions (e.g. "The above
1298
- exception was the direct cause of the following exception")."""
1299
-
1300
- chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]]
1301
-
1302
- def __init__(
1303
- self,
1304
- chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]],
1305
- ) -> None:
1306
- # reprcrash and reprtraceback of the outermost (the newest) exception
1307
- # in the chain.
1308
- super().__init__(
1309
- reprtraceback=chain[-1][0],
1310
- reprcrash=chain[-1][1],
1311
- )
1312
- self.chain = chain
1313
-
1314
- def toterminal(self, tw: TerminalWriter) -> None:
1315
- for reprtraceback, reprcrash, description in self.chain:
1316
- reprtraceback.toterminal(tw)
1317
- if description is not None:
1318
- tw.line("")
1319
- tw.line(description, yellow=True)
1320
- super().toterminal(tw)
1321
-
1322
-
1323
- @dataclasses.dataclass(eq=False)
1324
- class ReprExceptionInfo(ExceptionRepr):
1325
- """A single exception with optional extra details (function arguments,
1326
- function locals, file location) and possible extra line and sections emitted
1327
- at the end."""
1328
-
1329
- def toterminal(self, tw: TerminalWriter) -> None:
1330
- self.reprtraceback.toterminal(tw)
1331
- super().toterminal(tw)
1332
-
1333
-
1334
- @dataclasses.dataclass(eq=False)
1335
- class ReprTraceback(TerminalRepr):
1336
- """A traceback with optional extra details (function arguments, function
1337
- locals, file location) and possible extra line emitted at the end."""
1338
-
1339
- reprentries: Sequence[ReprEntry | ReprEntryNative]
1340
- extraline: str | None
1341
- style: TracebackStyle
1342
-
1343
- entrysep: ClassVar = "_ "
1344
-
1345
- def toterminal(self, tw: TerminalWriter) -> None:
1346
- # The entries might have different styles.
1347
- for i, entry in enumerate(self.reprentries):
1348
- if entry.style == "long":
1349
- tw.line("")
1350
- entry.toterminal(tw)
1351
- if i < len(self.reprentries) - 1:
1352
- next_entry = self.reprentries[i + 1]
1353
- if entry.style == "long" or (
1354
- entry.style == "short" and next_entry.style == "long"
1355
- ):
1356
- tw.sep(self.entrysep)
1357
-
1358
- if self.extraline:
1359
- tw.line(self.extraline)
1360
-
1361
-
1362
- class ReprTracebackNative(ReprTraceback):
1363
- """A traceback in native style.
1364
-
1365
- The lines are emitted as is; uses a single entry for the entire native
1366
- traceback.
1367
- """
1368
-
1369
- def __init__(self, tblines: Sequence[str], *, extraline: str | None = None) -> None:
1370
- self.reprentries = [ReprEntryNative(tblines)]
1371
- self.extraline = extraline
1372
- self.style = "native"
1373
-
1374
-
1375
- @dataclasses.dataclass(eq=False)
1376
- class ReprEntryNative(TerminalRepr):
1377
- """An entry in a traceback in native style.
1378
-
1379
- Emits the lines as is. The lines are assumed to include the line separators.
1380
-
1381
- [Note that we currently use a single "entry" for the entire native
1382
- traceback, so this is a bit misleading, but there's no point trying to parse
1383
- or split a native traceback.]
1384
- """
1385
-
1386
- lines: Sequence[str]
1387
-
1388
- style: ClassVar[TracebackStyle] = "native"
1389
-
1390
- def toterminal(self, tw: TerminalWriter) -> None:
1391
- tw.write("".join(self.lines))
1392
-
1393
-
1394
- @dataclasses.dataclass(eq=False)
1395
- class ReprEntry(TerminalRepr):
1396
- """An entry in a traceback with possible extra details (function arguments,
1397
- function locals, source snippet, function's file and line)."""
1398
-
1399
- lines: Sequence[str]
1400
- reprfuncargs: ReprFuncArgs | None
1401
- reprlocals: ReprLocals | None
1402
- reprfileloc: ReprFileLocation | None
1403
- style: TracebackStyle
1404
-
1405
- def _write_entry_lines(self, tw: TerminalWriter) -> None:
1406
- """Write the source code portions of a list of traceback entries with syntax highlighting.
1407
-
1408
- Usually entries are lines like these:
1409
-
1410
- " x = 1"
1411
- "> assert x == 2"
1412
- "E assert 1 == 2"
1413
-
1414
- This function takes care of rendering the "source" portions of it (the lines without
1415
- the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
1416
- character, as doing so might break line continuations.
1417
- """
1418
- if not self.lines:
1419
- return
1420
-
1421
- if self.style == "value":
1422
- # Using tw.write instead of tw.line for testing purposes due to TWMock implementation;
1423
- # lines written with TWMock.line and TWMock._write_source cannot be distinguished
1424
- # from each other, whereas lines written with TWMock.write are marked with TWMock.WRITE
1425
- for line in self.lines:
1426
- tw.write(line)
1427
- tw.write("\n")
1428
- return
1429
-
1430
- # separate indents and source lines that are not failures: we want to
1431
- # highlight the code but not the indentation, which may contain markers
1432
- # such as "> assert 0"
1433
- fail_marker = f"{ExceptionInfoFormatter.fail_marker} "
1434
- indent_size = len(fail_marker)
1435
- indents: list[str] = []
1436
- source_lines: list[str] = []
1437
- failure_lines: list[str] = []
1438
- for index, line in enumerate(self.lines):
1439
- is_failure_line = line.startswith(fail_marker)
1440
- if is_failure_line:
1441
- # from this point on all lines are considered part of the failure
1442
- failure_lines.extend(self.lines[index:])
1443
- break
1444
- else:
1445
- indents.append(line[:indent_size])
1446
- source_lines.append(line[indent_size:])
1447
-
1448
- tw._write_source(source_lines, indents)
1449
-
1450
- # failure lines are always completely red and bold
1451
- for line in failure_lines:
1452
- tw.line(line, bold=True, red=True)
1453
-
1454
- def toterminal(self, tw: TerminalWriter) -> None:
1455
- if self.style == "short":
1456
- if self.reprfileloc:
1457
- self.reprfileloc.toterminal(tw)
1458
- self._write_entry_lines(tw)
1459
- if self.reprlocals:
1460
- self.reprlocals.toterminal(tw, indent=" " * 8)
1461
- return
1462
-
1463
- if self.reprfuncargs:
1464
- self.reprfuncargs.toterminal(tw)
1465
-
1466
- self._write_entry_lines(tw)
1467
-
1468
- if self.reprlocals:
1469
- tw.line("")
1470
- self.reprlocals.toterminal(tw)
1471
- if self.reprfileloc:
1472
- if self.lines:
1473
- tw.line("")
1474
- self.reprfileloc.toterminal(tw)
1475
-
1476
- def __str__(self) -> str:
1477
- return "{}\n{}\n{}".format(
1478
- "\n".join(self.lines), self.reprlocals, self.reprfileloc
1479
- )
1480
-
1481
-
1482
- @dataclasses.dataclass(eq=False)
1483
- class ReprFileLocation(TerminalRepr):
1484
- """A message at a file location, using the `<path>:<lineno>: <message>`
1485
- format that most editors understand.
1486
-
1487
- Only the first line of the message is emitted.
1488
- """
1489
-
1490
- path: str
1491
- lineno: int
1492
- message: str
1493
-
1494
- def __post_init__(self) -> None:
1495
- self.path = str(self.path)
1496
-
1497
- def toterminal(self, tw: TerminalWriter) -> None:
1498
- msg = self.message
1499
- i = msg.find("\n")
1500
- if i != -1:
1501
- msg = msg[:i]
1502
- tw.write(self.path, bold=True, red=True)
1503
- tw.line(f":{self.lineno}: {msg}")
1504
-
1505
-
1506
- @dataclasses.dataclass(eq=False)
1507
- class ReprLocals(TerminalRepr):
1508
- """Function local variables (pre-formatted)."""
1509
-
1510
- lines: Sequence[str]
1511
-
1512
- def toterminal(self, tw: TerminalWriter, indent: str = "") -> None:
1513
- for line in self.lines:
1514
- tw.line(indent + line)
1515
-
1516
-
1517
- @dataclasses.dataclass(eq=False)
1518
- class ReprFuncArgs(TerminalRepr):
1519
- """Function arguments - name = value, comma separated."""
1520
-
1521
- args: Sequence[tuple[str, object]]
1522
-
1523
- def toterminal(self, tw: TerminalWriter) -> None:
1524
- if self.args:
1525
- linesofar = ""
1526
- for name, value in self.args:
1527
- ns = f"{name} = {value}"
1528
- if len(ns) + len(linesofar) + 2 > tw.fullwidth:
1529
- if linesofar:
1530
- tw.line(linesofar)
1531
- linesofar = ns
1532
- else:
1533
- if linesofar:
1534
- linesofar += ", " + ns
1535
- else:
1536
- linesofar = ns
1537
- if linesofar:
1538
- tw.line(linesofar)
1539
- tw.line("")
1540
-
1541
-
1542
- def getfslineno(obj: object) -> tuple[str | Path, int]:
1543
- """Return source location (path, lineno) for the given object.
1544
-
1545
- If the source cannot be determined return ("", -1).
1546
-
1547
- The line number is 0-based.
1548
- """
1549
- # xxx let decorators etc specify a sane ordering
1550
- # NOTE: this used to be done in _pytest.compat.getfslineno, initially added
1551
- # in 6ec13a2b9. It ("place_as") appears to be something very custom.
1552
- obj = get_real_func(obj)
1553
- if hasattr(obj, "place_as"):
1554
- obj = obj.place_as
1555
-
1556
- try:
1557
- code = Code.from_function(obj)
1558
- except TypeError:
1559
- try:
1560
- fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
1561
- except TypeError:
1562
- return "", -1
1563
-
1564
- fspath = (fn and absolutepath(fn)) or ""
1565
- lineno = -1
1566
- if fspath:
1567
- try:
1568
- _, lineno = findsource(obj)
1569
- except OSError:
1570
- pass
1571
- return fspath, lineno
1572
-
1573
- return code.path, code.firstlineno
1574
-
1575
-
1576
- def _byte_offset_to_character_offset(str, offset):
1577
- """Converts a byte based offset in a string to a code-point."""
1578
- as_utf8 = str.encode("utf-8")
1579
- return len(as_utf8[:offset].decode("utf-8", errors="replace"))
1580
-
1581
-
1582
- # Relative paths that we use to filter traceback entries from appearing to the user;
1583
- # see filter_traceback.
1584
- # note: if we need to add more paths than what we have now we should probably use a list
1585
- # for better maintenance.
1586
-
1587
- _PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
1588
- # pluggy is either a package or a single module depending on the version
1589
- if _PLUGGY_DIR.name == "__init__.py":
1590
- _PLUGGY_DIR = _PLUGGY_DIR.parent
1591
- _PYTEST_DIR = Path(_pytest.__file__).parent
1592
-
1593
-
1594
- def filter_traceback(entry: TracebackEntry) -> bool:
1595
- """Return True if a TracebackEntry instance should be included in tracebacks.
1596
-
1597
- We hide traceback entries of:
1598
-
1599
- * dynamically generated code (no code to show up for it);
1600
- * internal traceback from pytest or its internal libraries, py and pluggy.
1601
- """
1602
- # entry.path might sometimes return a str object when the entry
1603
- # points to dynamically generated code.
1604
- # See https://bitbucket.org/pytest-dev/py/issues/71.
1605
- raw_filename = entry.frame.code.raw.co_filename
1606
- is_generated = "<" in raw_filename and ">" in raw_filename
1607
- if is_generated:
1608
- return False
1609
-
1610
- # entry.path might point to a non-existing file, in which case it will
1611
- # also return a str object. See #1133.
1612
- p = Path(entry.path)
1613
-
1614
- parents = p.parents
1615
- if _PLUGGY_DIR in parents:
1616
- return False
1617
- if _PYTEST_DIR in parents:
1618
- return False
1619
-
1620
- return True
1621
-
1622
-
1623
- def filter_excinfo_traceback(
1624
- tbfilter: TracebackFilter, excinfo: ExceptionInfo[BaseException]
1625
- ) -> Traceback:
1626
- """Filter the exception traceback in ``excinfo`` according to ``tbfilter``."""
1627
- if callable(tbfilter):
1628
- return tbfilter(excinfo)
1629
- elif tbfilter:
1630
- return excinfo.traceback.filter(excinfo)
1631
- else:
1632
- return excinfo.traceback
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_code/source.py DELETED
@@ -1,228 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- from __future__ import annotations
3
-
4
- import ast
5
- from bisect import bisect_right
6
- from collections.abc import Iterable
7
- from collections.abc import Iterator
8
- import inspect
9
- import textwrap
10
- import tokenize
11
- import types
12
- from typing import overload
13
- import warnings
14
-
15
-
16
- class Source:
17
- """An immutable object holding a source code fragment.
18
-
19
- When using Source(...), the source lines are deindented.
20
- """
21
-
22
- def __init__(self, obj: object = None) -> None:
23
- if not obj:
24
- self.lines: list[str] = []
25
- self.raw_lines: list[str] = []
26
- elif isinstance(obj, Source):
27
- self.lines = obj.lines
28
- self.raw_lines = obj.raw_lines
29
- elif isinstance(obj, tuple | list):
30
- self.lines = deindent(x.rstrip("\n") for x in obj)
31
- self.raw_lines = list(x.rstrip("\n") for x in obj)
32
- elif isinstance(obj, str):
33
- self.lines = deindent(obj.split("\n"))
34
- self.raw_lines = obj.split("\n")
35
- else:
36
- try:
37
- rawcode = getrawcode(obj)
38
- src = inspect.getsource(rawcode)
39
- except TypeError:
40
- src = inspect.getsource(obj) # type: ignore[arg-type]
41
- self.lines = deindent(src.split("\n"))
42
- self.raw_lines = src.split("\n")
43
-
44
- def __eq__(self, other: object) -> bool:
45
- if not isinstance(other, Source):
46
- return NotImplemented
47
- return self.lines == other.lines
48
-
49
- # Ignore type because of https://github.com/python/mypy/issues/4266.
50
- __hash__ = None # type: ignore
51
-
52
- @overload
53
- def __getitem__(self, key: int) -> str: ...
54
-
55
- @overload
56
- def __getitem__(self, key: slice) -> Source: ...
57
-
58
- def __getitem__(self, key: int | slice) -> str | Source:
59
- if isinstance(key, int):
60
- return self.lines[key]
61
- else:
62
- if key.step not in (None, 1):
63
- raise IndexError("cannot slice a Source with a step")
64
- newsource = Source()
65
- newsource.lines = self.lines[key.start : key.stop]
66
- newsource.raw_lines = self.raw_lines[key.start : key.stop]
67
- return newsource
68
-
69
- def __iter__(self) -> Iterator[str]:
70
- return iter(self.lines)
71
-
72
- def __len__(self) -> int:
73
- return len(self.lines)
74
-
75
- def strip(self) -> Source:
76
- """Return new Source object with trailing and leading blank lines removed."""
77
- start, end = 0, len(self)
78
- while start < end and not self.lines[start].strip():
79
- start += 1
80
- while end > start and not self.lines[end - 1].strip():
81
- end -= 1
82
- source = Source()
83
- source.raw_lines = self.raw_lines
84
- source.lines[:] = self.lines[start:end]
85
- return source
86
-
87
- def indent(self, indent: str = " " * 4) -> Source:
88
- """Return a copy of the source object with all lines indented by the
89
- given indent-string."""
90
- newsource = Source()
91
- newsource.raw_lines = self.raw_lines
92
- newsource.lines = [(indent + line) for line in self.lines]
93
- return newsource
94
-
95
- def getstatement(self, lineno: int) -> Source:
96
- """Return Source statement which contains the given linenumber
97
- (counted from 0)."""
98
- start, end = self.getstatementrange(lineno)
99
- return self[start:end]
100
-
101
- def getstatementrange(self, lineno: int) -> tuple[int, int]:
102
- """Return (start, end) tuple which spans the minimal statement region
103
- which containing the given lineno."""
104
- if not (0 <= lineno < len(self)):
105
- raise IndexError("lineno out of range")
106
- _ast, start, end = getstatementrange_ast(lineno, self)
107
- return start, end
108
-
109
- def deindent(self) -> Source:
110
- """Return a new Source object deindented."""
111
- newsource = Source()
112
- newsource.lines[:] = deindent(self.lines)
113
- newsource.raw_lines = self.raw_lines
114
- return newsource
115
-
116
- def __str__(self) -> str:
117
- return "\n".join(self.lines)
118
-
119
-
120
- #
121
- # helper functions
122
- #
123
-
124
-
125
- def findsource(obj) -> tuple[Source | None, int]:
126
- try:
127
- sourcelines, lineno = inspect.findsource(obj)
128
- except Exception:
129
- return None, -1
130
- source = Source()
131
- source.lines = [line.rstrip() for line in sourcelines]
132
- source.raw_lines = sourcelines
133
- return source, lineno
134
-
135
-
136
- def getrawcode(obj: object, trycall: bool = True) -> types.CodeType:
137
- """Return code object for given function."""
138
- try:
139
- return obj.__code__ # type: ignore[attr-defined,no-any-return]
140
- except AttributeError:
141
- pass
142
- if trycall:
143
- call = getattr(obj, "__call__", None)
144
- if call and not isinstance(obj, type):
145
- return getrawcode(call, trycall=False)
146
- raise TypeError(f"could not get code object for {obj!r}")
147
-
148
-
149
- def deindent(lines: Iterable[str]) -> list[str]:
150
- return textwrap.dedent("\n".join(lines)).splitlines()
151
-
152
-
153
- def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None]:
154
- # Flatten all statements and except handlers into one lineno-list.
155
- # AST's line numbers start indexing at 1.
156
- values: list[int] = []
157
- for x in ast.walk(node):
158
- if isinstance(x, ast.stmt | ast.ExceptHandler):
159
- # The lineno points to the class/def, so need to include the decorators.
160
- if isinstance(x, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef):
161
- for d in x.decorator_list:
162
- values.append(d.lineno - 1)
163
- values.append(x.lineno - 1)
164
- for name in ("finalbody", "orelse"):
165
- val: list[ast.stmt] | None = getattr(x, name, None)
166
- if val:
167
- # Treat the finally/orelse part as its own statement.
168
- values.append(val[0].lineno - 1 - 1)
169
- values.sort()
170
- insert_index = bisect_right(values, lineno)
171
- if insert_index == 0:
172
- return 0, None
173
- start = values[insert_index - 1]
174
- if insert_index >= len(values):
175
- end = None
176
- else:
177
- end = values[insert_index]
178
- return start, end
179
-
180
-
181
- def getstatementrange_ast(
182
- lineno: int,
183
- source: Source,
184
- assertion: bool = False,
185
- astnode: ast.AST | None = None,
186
- ) -> tuple[ast.AST, int, int]:
187
- if astnode is None:
188
- content = str(source)
189
- # See #4260:
190
- # Don't produce duplicate warnings when compiling source to find AST.
191
- with warnings.catch_warnings():
192
- warnings.simplefilter("ignore")
193
- astnode = ast.parse(content, "source", "exec")
194
-
195
- start, end = get_statement_startend2(lineno, astnode)
196
- # We need to correct the end:
197
- # - ast-parsing strips comments
198
- # - there might be empty lines
199
- # - we might have lesser indented code blocks at the end
200
- if end is None:
201
- end = len(source.lines)
202
-
203
- if end > start + 1:
204
- # Make sure we don't span differently indented code blocks
205
- # by using the BlockFinder helper used which inspect.getsource() uses itself.
206
- block_finder = inspect.BlockFinder()
207
- # If we start with an indented line, put blockfinder to "started" mode.
208
- block_finder.started = (
209
- bool(source.lines[start]) and source.lines[start][0].isspace()
210
- )
211
- it = ((x + "\n") for x in source.lines[start:end])
212
- try:
213
- for tok in tokenize.generate_tokens(lambda: next(it)):
214
- block_finder.tokeneater(*tok)
215
- except (inspect.EndOfBlock, IndentationError):
216
- end = block_finder.last + start
217
- except Exception:
218
- pass
219
-
220
- # The end might still point to a comment or empty line, correct it.
221
- end = min(end, len(source.lines))
222
- while end:
223
- line = source.lines[end - 1].lstrip()
224
- if line.startswith("#") or not line:
225
- end -= 1
226
- else:
227
- break
228
- return astnode, start, end
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_io/__init__.py DELETED
@@ -1,10 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from .terminalwriter import get_terminal_width
4
- from .terminalwriter import TerminalWriter
5
-
6
-
7
- __all__ = [
8
- "TerminalWriter",
9
- "get_terminal_width",
10
- ]
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_io/pprint.py DELETED
@@ -1,673 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- # This module was imported from the cpython standard library
3
- # (https://github.com/python/cpython/) at commit
4
- # c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
5
- #
6
- #
7
- # Original Author: Fred L. Drake, Jr.
8
- # fdrake@acm.org
9
- #
10
- # This is a simple little module I wrote to make life easier. I didn't
11
- # see anything quite like it in the library, though I may have overlooked
12
- # something. I wrote this when I was trying to read some heavily nested
13
- # tuples with fairly non-descriptive content. This is modeled very much
14
- # after Lisp/Scheme - style pretty-printing of lists. If you find it
15
- # useful, thank small children who sleep at night.
16
- from __future__ import annotations
17
-
18
- import collections as _collections
19
- from collections.abc import Callable
20
- from collections.abc import Iterator
21
- import dataclasses as _dataclasses
22
- from io import StringIO as _StringIO
23
- import re
24
- import types as _types
25
- from typing import Any
26
- from typing import IO
27
-
28
-
29
- class _safe_key:
30
- """Helper function for key functions when sorting unorderable objects.
31
-
32
- The wrapped-object will fallback to a Py2.x style comparison for
33
- unorderable types (sorting first comparing the type name and then by
34
- the obj ids). Does not work recursively, so dict.items() must have
35
- _safe_key applied to both the key and the value.
36
-
37
- """
38
-
39
- __slots__ = ["obj"]
40
-
41
- def __init__(self, obj):
42
- self.obj = obj
43
-
44
- def __lt__(self, other):
45
- try:
46
- return self.obj < other.obj
47
- except TypeError:
48
- return (str(type(self.obj)), id(self.obj)) < (
49
- str(type(other.obj)),
50
- id(other.obj),
51
- )
52
-
53
-
54
- def _safe_tuple(t):
55
- """Helper function for comparing 2-tuples"""
56
- return _safe_key(t[0]), _safe_key(t[1])
57
-
58
-
59
- class PrettyPrinter:
60
- def __init__(
61
- self,
62
- indent: int = 4,
63
- width: int = 80,
64
- depth: int | None = None,
65
- ) -> None:
66
- """Handle pretty printing operations onto a stream using a set of
67
- configured parameters.
68
-
69
- indent
70
- Number of spaces to indent for each level of nesting.
71
-
72
- width
73
- Attempted maximum number of columns in the output.
74
-
75
- depth
76
- The maximum depth to print out nested structures.
77
-
78
- """
79
- if indent < 0:
80
- raise ValueError("indent must be >= 0")
81
- if depth is not None and depth <= 0:
82
- raise ValueError("depth must be > 0")
83
- if not width:
84
- raise ValueError("width must be != 0")
85
- self._depth = depth
86
- self._indent_per_level = indent
87
- self._width = width
88
-
89
- def pformat(self, object: Any) -> str:
90
- sio = _StringIO()
91
- self._format(object, sio, 0, 0, set(), 0)
92
- return sio.getvalue()
93
-
94
- def _format(
95
- self,
96
- object: Any,
97
- stream: IO[str],
98
- indent: int,
99
- allowance: int,
100
- context: set[int],
101
- level: int,
102
- ) -> None:
103
- objid = id(object)
104
- if objid in context:
105
- stream.write(_recursion(object))
106
- return
107
-
108
- p = self._dispatch.get(type(object).__repr__, None)
109
- if p is not None:
110
- context.add(objid)
111
- p(self, object, stream, indent, allowance, context, level + 1)
112
- context.remove(objid)
113
- elif (
114
- _dataclasses.is_dataclass(object)
115
- and not isinstance(object, type)
116
- and object.__dataclass_params__.repr # type:ignore[attr-defined]
117
- and
118
- # Check dataclass has generated repr method.
119
- hasattr(object.__repr__, "__wrapped__")
120
- and "__create_fn__" in object.__repr__.__wrapped__.__qualname__
121
- ):
122
- context.add(objid)
123
- self._pprint_dataclass(
124
- object, stream, indent, allowance, context, level + 1
125
- )
126
- context.remove(objid)
127
- else:
128
- stream.write(self._repr(object, context, level))
129
-
130
- def _pprint_dataclass(
131
- self,
132
- object: Any,
133
- stream: IO[str],
134
- indent: int,
135
- allowance: int,
136
- context: set[int],
137
- level: int,
138
- ) -> None:
139
- cls_name = object.__class__.__name__
140
- items = [
141
- (f.name, getattr(object, f.name))
142
- for f in _dataclasses.fields(object)
143
- if f.repr
144
- ]
145
- stream.write(cls_name + "(")
146
- self._format_namespace_items(items, stream, indent, allowance, context, level)
147
- stream.write(")")
148
-
149
- _dispatch: dict[
150
- Callable[..., str],
151
- Callable[[PrettyPrinter, Any, IO[str], int, int, set[int], int], None],
152
- ] = {}
153
-
154
- def _pprint_dict(
155
- self,
156
- object: Any,
157
- stream: IO[str],
158
- indent: int,
159
- allowance: int,
160
- context: set[int],
161
- level: int,
162
- ) -> None:
163
- write = stream.write
164
- write("{")
165
- items = object.items()
166
- self._format_dict_items(items, stream, indent, allowance, context, level)
167
- write("}")
168
-
169
- _dispatch[dict.__repr__] = _pprint_dict
170
-
171
- def _pprint_ordered_dict(
172
- self,
173
- object: Any,
174
- stream: IO[str],
175
- indent: int,
176
- allowance: int,
177
- context: set[int],
178
- level: int,
179
- ) -> None:
180
- if not len(object):
181
- stream.write(repr(object))
182
- return
183
- cls = object.__class__
184
- stream.write(cls.__name__ + "(")
185
- self._pprint_dict(object, stream, indent, allowance, context, level)
186
- stream.write(")")
187
-
188
- _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
189
-
190
- def _pprint_list(
191
- self,
192
- object: Any,
193
- stream: IO[str],
194
- indent: int,
195
- allowance: int,
196
- context: set[int],
197
- level: int,
198
- ) -> None:
199
- stream.write("[")
200
- self._format_items(object, stream, indent, allowance, context, level)
201
- stream.write("]")
202
-
203
- _dispatch[list.__repr__] = _pprint_list
204
-
205
- def _pprint_tuple(
206
- self,
207
- object: Any,
208
- stream: IO[str],
209
- indent: int,
210
- allowance: int,
211
- context: set[int],
212
- level: int,
213
- ) -> None:
214
- stream.write("(")
215
- self._format_items(object, stream, indent, allowance, context, level)
216
- stream.write(")")
217
-
218
- _dispatch[tuple.__repr__] = _pprint_tuple
219
-
220
- def _pprint_set(
221
- self,
222
- object: Any,
223
- stream: IO[str],
224
- indent: int,
225
- allowance: int,
226
- context: set[int],
227
- level: int,
228
- ) -> None:
229
- if not len(object):
230
- stream.write(repr(object))
231
- return
232
- typ = object.__class__
233
- if typ is set:
234
- stream.write("{")
235
- endchar = "}"
236
- else:
237
- stream.write(typ.__name__ + "({")
238
- endchar = "})"
239
- object = sorted(object, key=_safe_key)
240
- self._format_items(object, stream, indent, allowance, context, level)
241
- stream.write(endchar)
242
-
243
- _dispatch[set.__repr__] = _pprint_set
244
- _dispatch[frozenset.__repr__] = _pprint_set
245
-
246
- def _pprint_str(
247
- self,
248
- object: Any,
249
- stream: IO[str],
250
- indent: int,
251
- allowance: int,
252
- context: set[int],
253
- level: int,
254
- ) -> None:
255
- write = stream.write
256
- if not len(object):
257
- write(repr(object))
258
- return
259
- chunks = []
260
- lines = object.splitlines(True)
261
- if level == 1:
262
- indent += 1
263
- allowance += 1
264
- max_width1 = max_width = self._width - indent
265
- for i, line in enumerate(lines):
266
- rep = repr(line)
267
- if i == len(lines) - 1:
268
- max_width1 -= allowance
269
- if len(rep) <= max_width1:
270
- chunks.append(rep)
271
- else:
272
- # A list of alternating (non-space, space) strings
273
- parts = re.findall(r"\S*\s*", line)
274
- assert parts
275
- assert not parts[-1]
276
- parts.pop() # drop empty last part
277
- max_width2 = max_width
278
- current = ""
279
- for j, part in enumerate(parts):
280
- candidate = current + part
281
- if j == len(parts) - 1 and i == len(lines) - 1:
282
- max_width2 -= allowance
283
- if len(repr(candidate)) > max_width2:
284
- if current:
285
- chunks.append(repr(current))
286
- current = part
287
- else:
288
- current = candidate
289
- if current:
290
- chunks.append(repr(current))
291
- if len(chunks) == 1:
292
- write(rep)
293
- return
294
- if level == 1:
295
- write("(")
296
- for i, rep in enumerate(chunks):
297
- if i > 0:
298
- write("\n" + " " * indent)
299
- write(rep)
300
- if level == 1:
301
- write(")")
302
-
303
- _dispatch[str.__repr__] = _pprint_str
304
-
305
- def _pprint_bytes(
306
- self,
307
- object: Any,
308
- stream: IO[str],
309
- indent: int,
310
- allowance: int,
311
- context: set[int],
312
- level: int,
313
- ) -> None:
314
- write = stream.write
315
- if len(object) <= 4:
316
- write(repr(object))
317
- return
318
- parens = level == 1
319
- if parens:
320
- indent += 1
321
- allowance += 1
322
- write("(")
323
- delim = ""
324
- for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
325
- write(delim)
326
- write(rep)
327
- if not delim:
328
- delim = "\n" + " " * indent
329
- if parens:
330
- write(")")
331
-
332
- _dispatch[bytes.__repr__] = _pprint_bytes
333
-
334
- def _pprint_bytearray(
335
- self,
336
- object: Any,
337
- stream: IO[str],
338
- indent: int,
339
- allowance: int,
340
- context: set[int],
341
- level: int,
342
- ) -> None:
343
- write = stream.write
344
- write("bytearray(")
345
- self._pprint_bytes(
346
- bytes(object), stream, indent + 10, allowance + 1, context, level + 1
347
- )
348
- write(")")
349
-
350
- _dispatch[bytearray.__repr__] = _pprint_bytearray
351
-
352
- def _pprint_mappingproxy(
353
- self,
354
- object: Any,
355
- stream: IO[str],
356
- indent: int,
357
- allowance: int,
358
- context: set[int],
359
- level: int,
360
- ) -> None:
361
- stream.write("mappingproxy(")
362
- self._format(object.copy(), stream, indent, allowance, context, level)
363
- stream.write(")")
364
-
365
- _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
366
-
367
- def _pprint_simplenamespace(
368
- self,
369
- object: Any,
370
- stream: IO[str],
371
- indent: int,
372
- allowance: int,
373
- context: set[int],
374
- level: int,
375
- ) -> None:
376
- if type(object) is _types.SimpleNamespace:
377
- # The SimpleNamespace repr is "namespace" instead of the class
378
- # name, so we do the same here. For subclasses; use the class name.
379
- cls_name = "namespace"
380
- else:
381
- cls_name = object.__class__.__name__
382
- items = object.__dict__.items()
383
- stream.write(cls_name + "(")
384
- self._format_namespace_items(items, stream, indent, allowance, context, level)
385
- stream.write(")")
386
-
387
- _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
388
-
389
- def _format_dict_items(
390
- self,
391
- items: list[tuple[Any, Any]],
392
- stream: IO[str],
393
- indent: int,
394
- allowance: int,
395
- context: set[int],
396
- level: int,
397
- ) -> None:
398
- if not items:
399
- return
400
-
401
- write = stream.write
402
- item_indent = indent + self._indent_per_level
403
- delimnl = "\n" + " " * item_indent
404
- for key, ent in items:
405
- write(delimnl)
406
- write(self._repr(key, context, level))
407
- write(": ")
408
- self._format(ent, stream, item_indent, 1, context, level)
409
- write(",")
410
-
411
- write("\n" + " " * indent)
412
-
413
- def _format_namespace_items(
414
- self,
415
- items: list[tuple[Any, Any]],
416
- stream: IO[str],
417
- indent: int,
418
- allowance: int,
419
- context: set[int],
420
- level: int,
421
- ) -> None:
422
- if not items:
423
- return
424
-
425
- write = stream.write
426
- item_indent = indent + self._indent_per_level
427
- delimnl = "\n" + " " * item_indent
428
- for key, ent in items:
429
- write(delimnl)
430
- write(key)
431
- write("=")
432
- if id(ent) in context:
433
- # Special-case representation of recursion to match standard
434
- # recursive dataclass repr.
435
- write("...")
436
- else:
437
- self._format(
438
- ent,
439
- stream,
440
- item_indent + len(key) + 1,
441
- 1,
442
- context,
443
- level,
444
- )
445
-
446
- write(",")
447
-
448
- write("\n" + " " * indent)
449
-
450
- def _format_items(
451
- self,
452
- items: list[Any],
453
- stream: IO[str],
454
- indent: int,
455
- allowance: int,
456
- context: set[int],
457
- level: int,
458
- ) -> None:
459
- if not items:
460
- return
461
-
462
- write = stream.write
463
- item_indent = indent + self._indent_per_level
464
- delimnl = "\n" + " " * item_indent
465
-
466
- for item in items:
467
- write(delimnl)
468
- self._format(item, stream, item_indent, 1, context, level)
469
- write(",")
470
-
471
- write("\n" + " " * indent)
472
-
473
- def _repr(self, object: Any, context: set[int], level: int) -> str:
474
- return self._safe_repr(object, context.copy(), self._depth, level)
475
-
476
- def _pprint_default_dict(
477
- self,
478
- object: Any,
479
- stream: IO[str],
480
- indent: int,
481
- allowance: int,
482
- context: set[int],
483
- level: int,
484
- ) -> None:
485
- rdf = self._repr(object.default_factory, context, level)
486
- stream.write(f"{object.__class__.__name__}({rdf}, ")
487
- self._pprint_dict(object, stream, indent, allowance, context, level)
488
- stream.write(")")
489
-
490
- _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
491
-
492
- def _pprint_counter(
493
- self,
494
- object: Any,
495
- stream: IO[str],
496
- indent: int,
497
- allowance: int,
498
- context: set[int],
499
- level: int,
500
- ) -> None:
501
- stream.write(object.__class__.__name__ + "(")
502
-
503
- if object:
504
- stream.write("{")
505
- items = object.most_common()
506
- self._format_dict_items(items, stream, indent, allowance, context, level)
507
- stream.write("}")
508
-
509
- stream.write(")")
510
-
511
- _dispatch[_collections.Counter.__repr__] = _pprint_counter
512
-
513
- def _pprint_chain_map(
514
- self,
515
- object: Any,
516
- stream: IO[str],
517
- indent: int,
518
- allowance: int,
519
- context: set[int],
520
- level: int,
521
- ) -> None:
522
- if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
523
- stream.write(repr(object))
524
- return
525
-
526
- stream.write(object.__class__.__name__ + "(")
527
- self._format_items(object.maps, stream, indent, allowance, context, level)
528
- stream.write(")")
529
-
530
- _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
531
-
532
- def _pprint_deque(
533
- self,
534
- object: Any,
535
- stream: IO[str],
536
- indent: int,
537
- allowance: int,
538
- context: set[int],
539
- level: int,
540
- ) -> None:
541
- stream.write(object.__class__.__name__ + "(")
542
- if object.maxlen is not None:
543
- stream.write(f"maxlen={object.maxlen}, ")
544
- stream.write("[")
545
-
546
- self._format_items(object, stream, indent, allowance + 1, context, level)
547
- stream.write("])")
548
-
549
- _dispatch[_collections.deque.__repr__] = _pprint_deque
550
-
551
- def _pprint_user_dict(
552
- self,
553
- object: Any,
554
- stream: IO[str],
555
- indent: int,
556
- allowance: int,
557
- context: set[int],
558
- level: int,
559
- ) -> None:
560
- self._format(object.data, stream, indent, allowance, context, level - 1)
561
-
562
- _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict
563
-
564
- def _pprint_user_list(
565
- self,
566
- object: Any,
567
- stream: IO[str],
568
- indent: int,
569
- allowance: int,
570
- context: set[int],
571
- level: int,
572
- ) -> None:
573
- self._format(object.data, stream, indent, allowance, context, level - 1)
574
-
575
- _dispatch[_collections.UserList.__repr__] = _pprint_user_list
576
-
577
- def _pprint_user_string(
578
- self,
579
- object: Any,
580
- stream: IO[str],
581
- indent: int,
582
- allowance: int,
583
- context: set[int],
584
- level: int,
585
- ) -> None:
586
- self._format(object.data, stream, indent, allowance, context, level - 1)
587
-
588
- _dispatch[_collections.UserString.__repr__] = _pprint_user_string
589
-
590
- def _safe_repr(
591
- self, object: Any, context: set[int], maxlevels: int | None, level: int
592
- ) -> str:
593
- typ = type(object)
594
- if typ in _builtin_scalars:
595
- return repr(object)
596
-
597
- r = getattr(typ, "__repr__", None)
598
-
599
- if issubclass(typ, dict) and r is dict.__repr__:
600
- if not object:
601
- return "{}"
602
- objid = id(object)
603
- if maxlevels and level >= maxlevels:
604
- return "{...}"
605
- if objid in context:
606
- return _recursion(object)
607
- context.add(objid)
608
- components: list[str] = []
609
- append = components.append
610
- level += 1
611
- for k, v in object.items():
612
- krepr = self._safe_repr(k, context, maxlevels, level)
613
- vrepr = self._safe_repr(v, context, maxlevels, level)
614
- append(f"{krepr}: {vrepr}")
615
- context.remove(objid)
616
- return "{{{}}}".format(", ".join(components))
617
-
618
- if (issubclass(typ, list) and r is list.__repr__) or (
619
- issubclass(typ, tuple) and r is tuple.__repr__
620
- ):
621
- if issubclass(typ, list):
622
- if not object:
623
- return "[]"
624
- format = "[%s]"
625
- elif len(object) == 1:
626
- format = "(%s,)"
627
- else:
628
- if not object:
629
- return "()"
630
- format = "(%s)"
631
- objid = id(object)
632
- if maxlevels and level >= maxlevels:
633
- return format % "..."
634
- if objid in context:
635
- return _recursion(object)
636
- context.add(objid)
637
- components = []
638
- append = components.append
639
- level += 1
640
- for o in object:
641
- orepr = self._safe_repr(o, context, maxlevels, level)
642
- append(orepr)
643
- context.remove(objid)
644
- return format % ", ".join(components)
645
-
646
- return repr(object)
647
-
648
-
649
- _builtin_scalars = frozenset(
650
- {str, bytes, bytearray, float, complex, bool, type(None), int}
651
- )
652
-
653
-
654
- def _recursion(object: Any) -> str:
655
- return f"<Recursion on {type(object).__name__} with id={id(object)}>"
656
-
657
-
658
- def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]:
659
- current = b""
660
- last = len(object) // 4 * 4
661
- for i in range(0, len(object), 4):
662
- part = object[i : i + 4]
663
- candidate = current + part
664
- if i == last:
665
- width -= allowance
666
- if len(repr(candidate)) > width:
667
- if current:
668
- yield repr(current)
669
- current = part
670
- else:
671
- current = candidate
672
- if current:
673
- yield repr(current)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_io/saferepr.py DELETED
@@ -1,155 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from itertools import islice
4
- import pprint
5
- import reprlib
6
-
7
-
8
- def _try_repr_or_str(obj: object) -> str:
9
- try:
10
- return repr(obj)
11
- except (KeyboardInterrupt, SystemExit):
12
- raise
13
- except BaseException:
14
- return f'{type(obj).__name__}("{obj}")'
15
-
16
-
17
- def _format_repr_exception(exc: BaseException, obj: object) -> str:
18
- try:
19
- exc_info = _try_repr_or_str(exc)
20
- except (KeyboardInterrupt, SystemExit):
21
- raise
22
- except BaseException as inner_exc:
23
- exc_info = f"unpresentable exception ({_try_repr_or_str(inner_exc)})"
24
- return (
25
- f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
26
- )
27
-
28
-
29
- def _ellipsize(s: str, maxsize: int) -> str:
30
- if len(s) > maxsize:
31
- i = max(0, (maxsize - 3) // 2)
32
- j = max(0, maxsize - 3 - i)
33
- return s[:i] + "..." + s[len(s) - j :]
34
- return s
35
-
36
-
37
- class SafeRepr(reprlib.Repr):
38
- """
39
- repr.Repr that limits the resulting size of repr() and includes
40
- information on exceptions raised during the call.
41
- """
42
-
43
- def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None:
44
- """
45
- :param maxsize:
46
- If not None, will truncate the resulting repr to that specific size, using ellipsis
47
- somewhere in the middle to hide the extra text.
48
- If None, will not impose any size limits on the returning repr.
49
- """
50
- super().__init__()
51
- # ``maxstring`` is used by the superclass, and needs to be an int; using a
52
- # very large number in case maxsize is None, meaning we want to disable
53
- # truncation.
54
- self.maxstring = maxsize if maxsize is not None else 1_000_000_000
55
- self.maxsize = maxsize
56
- self.use_ascii = use_ascii
57
-
58
- def repr(self, x: object) -> str:
59
- try:
60
- if self.use_ascii:
61
- s = ascii(x)
62
- else:
63
- s = super().repr(x)
64
- except (KeyboardInterrupt, SystemExit):
65
- raise
66
- except BaseException as exc:
67
- s = _format_repr_exception(exc, x)
68
- if self.maxsize is not None:
69
- s = _ellipsize(s, self.maxsize)
70
- return s
71
-
72
- def repr_instance(self, x: object, level: int) -> str:
73
- try:
74
- s = repr(x)
75
- except (KeyboardInterrupt, SystemExit):
76
- raise
77
- except BaseException as exc:
78
- s = _format_repr_exception(exc, x)
79
- if self.maxsize is not None:
80
- s = _ellipsize(s, self.maxsize)
81
-
82
- return s
83
-
84
- def repr_dict(self, x: dict[object, object], level: int) -> str:
85
- """Represent a dict while preserving its insertion order.
86
-
87
- Differs from ``reprlib.Repr.repr_dict`` by iterating directly over ``x``
88
- rather than using the stdlib's sorting helper.
89
- """
90
- fillvalue = "..."
91
- n = len(x)
92
- if n == 0:
93
- return "{}"
94
- if level <= 0:
95
- return "{" + fillvalue + "}"
96
- newlevel = level - 1
97
- repr1 = self.repr1
98
- pieces = []
99
- for key in islice(x, self.maxdict):
100
- keyrepr = repr1(key, newlevel)
101
- valrepr = repr1(x[key], newlevel)
102
- pieces.append(f"{keyrepr}: {valrepr}")
103
- if n > self.maxdict:
104
- pieces.append(fillvalue)
105
- return "{" + ", ".join(pieces) + "}"
106
-
107
-
108
- def safeformat(obj: object) -> str:
109
- """Return a pretty printed string for the given object.
110
-
111
- Failing __repr__ functions of user instances will be represented
112
- with a short exception info.
113
- """
114
- try:
115
- return pprint.pformat(obj)
116
- except Exception as exc:
117
- return _format_repr_exception(exc, obj)
118
-
119
-
120
- # Maximum size of overall repr of objects to display during assertion errors.
121
- DEFAULT_REPR_MAX_SIZE = 240
122
-
123
-
124
- def saferepr(
125
- obj: object, maxsize: int | None = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
126
- ) -> str:
127
- """Return a size-limited safe repr-string for the given object.
128
-
129
- Failing __repr__ functions of user instances will be represented
130
- with a short exception info and 'saferepr' generally takes
131
- care to never raise exceptions itself.
132
-
133
- This function is a wrapper around the Repr/reprlib functionality of the
134
- stdlib.
135
- """
136
- return SafeRepr(maxsize, use_ascii).repr(obj)
137
-
138
-
139
- def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
140
- """Return an unlimited-size safe repr-string for the given object.
141
-
142
- As with saferepr, failing __repr__ functions of user instances
143
- will be represented with a short exception info.
144
-
145
- This function is a wrapper around simple repr.
146
-
147
- Note: a cleaner solution would be to alter ``saferepr``this way
148
- when maxsize=None, but that might affect some other code.
149
- """
150
- try:
151
- if use_ascii:
152
- return ascii(obj)
153
- return repr(obj)
154
- except Exception as exc:
155
- return _format_repr_exception(exc, obj)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_io/terminalwriter.py DELETED
@@ -1,258 +0,0 @@
1
- """Helper functions for writing to terminals and files."""
2
-
3
- from __future__ import annotations
4
-
5
- from collections.abc import Sequence
6
- import os
7
- import shutil
8
- import sys
9
- from typing import final
10
- from typing import Literal
11
- from typing import TextIO
12
-
13
- import pygments
14
- from pygments.formatters.terminal import TerminalFormatter
15
- from pygments.lexer import Lexer
16
- from pygments.lexers.diff import DiffLexer
17
- from pygments.lexers.python import PythonLexer
18
-
19
- from ..compat import assert_never
20
- from .wcwidth import wcswidth
21
-
22
-
23
- # This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
24
-
25
-
26
- def get_terminal_width() -> int:
27
- width, _ = shutil.get_terminal_size(fallback=(80, 24))
28
-
29
- # The Windows get_terminal_size may be bogus, let's sanify a bit.
30
- if width < 40:
31
- width = 80
32
-
33
- return width
34
-
35
-
36
- def should_do_markup(file: TextIO) -> bool:
37
- if os.environ.get("PY_COLORS") == "1":
38
- return True
39
- if os.environ.get("PY_COLORS") == "0":
40
- return False
41
- if os.environ.get("NO_COLOR"):
42
- return False
43
- if os.environ.get("FORCE_COLOR"):
44
- return True
45
- return (
46
- hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb"
47
- )
48
-
49
-
50
- @final
51
- class TerminalWriter:
52
- _esctable = dict(
53
- black=30,
54
- red=31,
55
- green=32,
56
- yellow=33,
57
- blue=34,
58
- purple=35,
59
- cyan=36,
60
- white=37,
61
- Black=40,
62
- Red=41,
63
- Green=42,
64
- Yellow=43,
65
- Blue=44,
66
- Purple=45,
67
- Cyan=46,
68
- White=47,
69
- bold=1,
70
- light=2,
71
- blink=5,
72
- invert=7,
73
- )
74
-
75
- def __init__(self, file: TextIO | None = None) -> None:
76
- if file is None:
77
- file = sys.stdout
78
- if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32":
79
- try:
80
- import colorama
81
- except ImportError:
82
- pass
83
- else:
84
- file = colorama.AnsiToWin32(file).stream
85
- assert file is not None
86
- self._file = file
87
- self.hasmarkup = should_do_markup(file)
88
- self._current_line = ""
89
- self._terminal_width: int | None = None
90
- self.code_highlight = True
91
-
92
- @property
93
- def fullwidth(self) -> int:
94
- if self._terminal_width is not None:
95
- return self._terminal_width
96
- return get_terminal_width()
97
-
98
- @fullwidth.setter
99
- def fullwidth(self, value: int) -> None:
100
- self._terminal_width = value
101
-
102
- @property
103
- def width_of_current_line(self) -> int:
104
- """Return an estimate of the width so far in the current line."""
105
- return wcswidth(self._current_line)
106
-
107
- def markup(self, text: str, **markup: bool) -> str:
108
- for name in markup:
109
- if name not in self._esctable:
110
- raise ValueError(f"unknown markup: {name!r}")
111
- if self.hasmarkup:
112
- esc = [self._esctable[name] for name, on in markup.items() if on]
113
- if esc:
114
- text = "".join(f"\x1b[{cod}m" for cod in esc) + text + "\x1b[0m"
115
- return text
116
-
117
- def sep(
118
- self,
119
- sepchar: str,
120
- title: str | None = None,
121
- fullwidth: int | None = None,
122
- **markup: bool,
123
- ) -> None:
124
- if fullwidth is None:
125
- fullwidth = self.fullwidth
126
- # The goal is to have the line be as long as possible
127
- # under the condition that len(line) <= fullwidth.
128
- if sys.platform == "win32":
129
- # If we print in the last column on windows we are on a
130
- # new line but there is no way to verify/neutralize this
131
- # (we may not know the exact line width).
132
- # So let's be defensive to avoid empty lines in the output.
133
- fullwidth -= 1
134
- if title is not None:
135
- # we want 2 + 2*len(fill) + len(title) <= fullwidth
136
- # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth
137
- # 2*len(sepchar)*N <= fullwidth - len(title) - 2
138
- # N <= (fullwidth - len(title) - 2) // (2*len(sepchar))
139
- N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1)
140
- fill = sepchar * N
141
- line = f"{fill} {title} {fill}"
142
- else:
143
- # we want len(sepchar)*N <= fullwidth
144
- # i.e. N <= fullwidth // len(sepchar)
145
- line = sepchar * (fullwidth // len(sepchar))
146
- # In some situations there is room for an extra sepchar at the right,
147
- # in particular if we consider that with a sepchar like "_ " the
148
- # trailing space is not important at the end of the line.
149
- if len(line) + len(sepchar.rstrip()) <= fullwidth:
150
- line += sepchar.rstrip()
151
-
152
- self.line(line, **markup)
153
-
154
- def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None:
155
- if msg:
156
- current_line = msg.rsplit("\n", 1)[-1]
157
- if "\n" in msg:
158
- self._current_line = current_line
159
- else:
160
- self._current_line += current_line
161
-
162
- msg = self.markup(msg, **markup)
163
-
164
- self.write_raw(msg, flush=flush)
165
-
166
- def write_raw(self, msg: str, *, flush: bool = False) -> None:
167
- try:
168
- self._file.write(msg)
169
- except UnicodeEncodeError:
170
- # Some environments don't support printing general Unicode
171
- # strings, due to misconfiguration or otherwise; in that case,
172
- # print the string escaped to ASCII.
173
- # When the Unicode situation improves we should consider
174
- # letting the error propagate instead of masking it (see #7475
175
- # for one brief attempt).
176
- msg = msg.encode("unicode-escape").decode("ascii")
177
- self._file.write(msg)
178
-
179
- if flush:
180
- self.flush()
181
-
182
- def line(self, s: str = "", **markup: bool) -> None:
183
- self.write(s, **markup)
184
- self.write("\n")
185
-
186
- def flush(self) -> None:
187
- self._file.flush()
188
-
189
- def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None:
190
- """Write lines of source code possibly highlighted.
191
-
192
- Keeping this private for now because the API is clunky. We should discuss how
193
- to evolve the terminal writer so we can have more precise color support, for example
194
- being able to write part of a line in one color and the rest in another, and so on.
195
- """
196
- if indents and len(indents) != len(lines):
197
- raise ValueError(
198
- f"indents size ({len(indents)}) should have same size as lines ({len(lines)})"
199
- )
200
- if not indents:
201
- indents = [""] * len(lines)
202
- source = "\n".join(lines)
203
- new_lines = self._highlight(source).splitlines()
204
- # Would be better to strict=True but that fails some CI jobs.
205
- for indent, new_line in zip(indents, new_lines, strict=False):
206
- self.line(indent + new_line)
207
-
208
- def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer:
209
- if lexer == "python":
210
- return PythonLexer()
211
- elif lexer == "diff":
212
- return DiffLexer()
213
- else:
214
- assert_never(lexer)
215
-
216
- def _get_pygments_formatter(self) -> TerminalFormatter:
217
- from _pytest.config.exceptions import UsageError
218
-
219
- theme = os.getenv("PYTEST_THEME")
220
- theme_mode = os.getenv("PYTEST_THEME_MODE", "dark")
221
-
222
- try:
223
- return TerminalFormatter(bg=theme_mode, style=theme)
224
- except pygments.util.ClassNotFound as e:
225
- raise UsageError(
226
- f"PYTEST_THEME environment variable has an invalid value: '{theme}'. "
227
- "Hint: See available pygments styles with `pygmentize -L styles`."
228
- ) from e
229
- except pygments.util.OptionError as e:
230
- raise UsageError(
231
- f"PYTEST_THEME_MODE environment variable has an invalid value: '{theme_mode}'. "
232
- "The allowed values are 'dark' (default) and 'light'."
233
- ) from e
234
-
235
- def _highlight(
236
- self, source: str, lexer: Literal["diff", "python"] = "python"
237
- ) -> str:
238
- """Highlight the given source if we have markup support."""
239
- if not source or not self.hasmarkup or not self.code_highlight:
240
- return source
241
-
242
- pygments_lexer = self._get_pygments_lexer(lexer)
243
- pygments_formatter = self._get_pygments_formatter()
244
-
245
- highlighted: str = pygments.highlight(
246
- source, pygments_lexer, pygments_formatter
247
- )
248
- # pygments terminal formatter may add a newline when there wasn't one.
249
- # We don't want this, remove.
250
- if highlighted[-1] == "\n" and source[-1] != "\n":
251
- highlighted = highlighted[:-1]
252
-
253
- # Some lexers will not set the initial color explicitly
254
- # which may lead to the previous color being propagated to the
255
- # start of the expression, so reset first.
256
- highlighted = "\x1b[0m" + highlighted
257
-
258
- return highlighted
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_io/wcwidth.py DELETED
@@ -1,57 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from functools import lru_cache
4
- import unicodedata
5
-
6
-
7
- @lru_cache(100)
8
- def wcwidth(c: str) -> int:
9
- """Determine how many columns are needed to display a character in a terminal.
10
-
11
- Returns -1 if the character is not printable.
12
- Returns 0, 1 or 2 for other characters.
13
- """
14
- o = ord(c)
15
-
16
- # ASCII fast path.
17
- if 0x20 <= o < 0x07F:
18
- return 1
19
-
20
- # Some Cf/Zp/Zl characters which should be zero-width.
21
- if (
22
- o == 0x0000
23
- or 0x200B <= o <= 0x200F
24
- or 0x2028 <= o <= 0x202E
25
- or 0x2060 <= o <= 0x2063
26
- ):
27
- return 0
28
-
29
- category = unicodedata.category(c)
30
-
31
- # Control characters.
32
- if category == "Cc":
33
- return -1
34
-
35
- # Combining characters with zero width.
36
- if category in ("Me", "Mn"):
37
- return 0
38
-
39
- # Full/Wide east asian characters.
40
- if unicodedata.east_asian_width(c) in ("F", "W"):
41
- return 2
42
-
43
- return 1
44
-
45
-
46
- def wcswidth(s: str) -> int:
47
- """Determine how many columns are needed to display a string in a terminal.
48
-
49
- Returns -1 if the string contains non-printable characters.
50
- """
51
- width = 0
52
- for c in unicodedata.normalize("NFC", s):
53
- wc = wcwidth(c)
54
- if wc < 0:
55
- return -1
56
- width += wc
57
- return width
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_py/__init__.py DELETED
File without changes
venv/Lib/site-packages/_pytest/_py/error.py DELETED
@@ -1,119 +0,0 @@
1
- """create errno-specific classes for IO or os calls."""
2
-
3
- from __future__ import annotations
4
-
5
- from collections.abc import Callable
6
- import errno
7
- import os
8
- import sys
9
- from typing import TYPE_CHECKING
10
- from typing import TypeVar
11
-
12
-
13
- if TYPE_CHECKING:
14
- from typing_extensions import ParamSpec
15
-
16
- P = ParamSpec("P")
17
-
18
- R = TypeVar("R")
19
-
20
-
21
- class Error(EnvironmentError):
22
- def __repr__(self) -> str:
23
- return "{}.{} {!r}: {} ".format(
24
- self.__class__.__module__,
25
- self.__class__.__name__,
26
- self.__class__.__doc__,
27
- " ".join(map(str, self.args)),
28
- # repr(self.args)
29
- )
30
-
31
- def __str__(self) -> str:
32
- s = "[{}]: {}".format(
33
- self.__class__.__doc__,
34
- " ".join(map(str, self.args)),
35
- )
36
- return s
37
-
38
-
39
- _winerrnomap = {
40
- 2: errno.ENOENT,
41
- 3: errno.ENOENT,
42
- 17: errno.EEXIST,
43
- 18: errno.EXDEV,
44
- 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable
45
- 22: errno.ENOTDIR,
46
- 20: errno.ENOTDIR,
47
- 267: errno.ENOTDIR,
48
- 5: errno.EACCES, # anything better?
49
- }
50
-
51
-
52
- class ErrorMaker:
53
- """lazily provides Exception classes for each possible POSIX errno
54
- (as defined per the 'errno' module). All such instances
55
- subclass EnvironmentError.
56
- """
57
-
58
- _errno2class: dict[int, type[Error]] = {}
59
-
60
- def __getattr__(self, name: str) -> type[Error]:
61
- if name[0] == "_":
62
- raise AttributeError(name)
63
- eno = getattr(errno, name)
64
- cls = self._geterrnoclass(eno)
65
- setattr(self, name, cls)
66
- return cls
67
-
68
- def _geterrnoclass(self, eno: int) -> type[Error]:
69
- try:
70
- return self._errno2class[eno]
71
- except KeyError:
72
- clsname = errno.errorcode.get(eno, f"UnknownErrno{eno}")
73
- errorcls = type(
74
- clsname,
75
- (Error,),
76
- {"__module__": "py.error", "__doc__": os.strerror(eno)},
77
- )
78
- self._errno2class[eno] = errorcls
79
- return errorcls
80
-
81
- def checked_call(
82
- self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
83
- ) -> R:
84
- """Call a function and raise an errno-exception if applicable."""
85
- __tracebackhide__ = True
86
- try:
87
- return func(*args, **kwargs)
88
- except Error:
89
- raise
90
- except OSError as value:
91
- if not hasattr(value, "errno"):
92
- raise
93
- if sys.platform == "win32":
94
- try:
95
- # error: Invalid index type "Optional[int]" for "dict[int, int]"; expected type "int" [index]
96
- # OK to ignore because we catch the KeyError below.
97
- cls = self._geterrnoclass(_winerrnomap[value.errno]) # type:ignore[index]
98
- except KeyError:
99
- raise value
100
- else:
101
- # we are not on Windows, or we got a proper OSError
102
- if value.errno is None:
103
- cls = type(
104
- "UnknownErrnoNone",
105
- (Error,),
106
- {"__module__": "py.error", "__doc__": None},
107
- )
108
- else:
109
- cls = self._geterrnoclass(value.errno)
110
-
111
- raise cls(f"{func.__name__}{args!r}")
112
-
113
-
114
- _error_maker = ErrorMaker()
115
- checked_call = _error_maker.checked_call
116
-
117
-
118
- def __getattr__(attr: str) -> type[Error]:
119
- return getattr(_error_maker, attr) # type: ignore[no-any-return]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_py/path.py DELETED
@@ -1,1475 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- """local path implementation."""
3
-
4
- from __future__ import annotations
5
-
6
- import atexit
7
- from collections.abc import Callable
8
- from contextlib import contextmanager
9
- import fnmatch
10
- import importlib.util
11
- import io
12
- import os
13
- from os.path import abspath
14
- from os.path import dirname
15
- from os.path import exists
16
- from os.path import isabs
17
- from os.path import isdir
18
- from os.path import isfile
19
- from os.path import islink
20
- from os.path import normpath
21
- import posixpath
22
- from stat import S_ISDIR
23
- from stat import S_ISLNK
24
- from stat import S_ISREG
25
- import sys
26
- from typing import Any
27
- from typing import cast
28
- from typing import Literal
29
- from typing import overload
30
- from typing import TYPE_CHECKING
31
- import uuid
32
- import warnings
33
-
34
- from . import error
35
-
36
-
37
- # Moved from local.py.
38
- iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
39
-
40
-
41
- class Checkers:
42
- _depend_on_existence = "exists", "link", "dir", "file"
43
-
44
- def __init__(self, path):
45
- self.path = path
46
-
47
- def dotfile(self):
48
- return self.path.basename.startswith(".")
49
-
50
- def ext(self, arg):
51
- if not arg.startswith("."):
52
- arg = "." + arg
53
- return self.path.ext == arg
54
-
55
- def basename(self, arg):
56
- return self.path.basename == arg
57
-
58
- def basestarts(self, arg):
59
- return self.path.basename.startswith(arg)
60
-
61
- def relto(self, arg):
62
- return self.path.relto(arg)
63
-
64
- def fnmatch(self, arg):
65
- return self.path.fnmatch(arg)
66
-
67
- def endswith(self, arg):
68
- return str(self.path).endswith(arg)
69
-
70
- def _evaluate(self, kw):
71
- from .._code.source import getrawcode
72
-
73
- for name, value in kw.items():
74
- invert = False
75
- meth = None
76
- try:
77
- meth = getattr(self, name)
78
- except AttributeError:
79
- if name[:3] == "not":
80
- invert = True
81
- try:
82
- meth = getattr(self, name[3:])
83
- except AttributeError:
84
- pass
85
- if meth is None:
86
- raise TypeError(f"no {name!r} checker available for {self.path!r}")
87
- try:
88
- if getrawcode(meth).co_argcount > 1:
89
- if (not meth(value)) ^ invert:
90
- return False
91
- else:
92
- if bool(value) ^ bool(meth()) ^ invert:
93
- return False
94
- except (error.ENOENT, error.ENOTDIR, error.EBUSY):
95
- # EBUSY feels not entirely correct,
96
- # but its kind of necessary since ENOMEDIUM
97
- # is not accessible in python
98
- for name in self._depend_on_existence:
99
- if name in kw:
100
- if kw.get(name):
101
- return False
102
- name = "not" + name
103
- if name in kw:
104
- if not kw.get(name):
105
- return False
106
- return True
107
-
108
- _statcache: Stat
109
-
110
- def _stat(self) -> Stat:
111
- try:
112
- return self._statcache
113
- except AttributeError:
114
- try:
115
- self._statcache = self.path.stat()
116
- except error.ELOOP:
117
- self._statcache = self.path.lstat()
118
- return self._statcache
119
-
120
- def dir(self):
121
- return S_ISDIR(self._stat().mode)
122
-
123
- def file(self):
124
- return S_ISREG(self._stat().mode)
125
-
126
- def exists(self):
127
- return self._stat()
128
-
129
- def link(self):
130
- st = self.path.lstat()
131
- return S_ISLNK(st.mode)
132
-
133
-
134
- class NeverRaised(Exception):
135
- pass
136
-
137
-
138
- class Visitor:
139
- def __init__(self, fil, rec, ignore, bf, sort):
140
- if isinstance(fil, (str, bytes)):
141
- fil = FNMatcher(fil)
142
- if isinstance(rec, str):
143
- self.rec: Callable[[LocalPath], bool] = FNMatcher(rec)
144
- elif not hasattr(rec, "__call__") and rec:
145
- self.rec = lambda path: True
146
- else:
147
- self.rec = rec
148
- self.fil = fil
149
- self.ignore = ignore
150
- self.breadthfirst = bf
151
- self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)
152
-
153
- def gen(self, path):
154
- try:
155
- entries = path.listdir()
156
- except self.ignore:
157
- return
158
- rec = self.rec
159
- dirs = self.optsort(
160
- [p for p in entries if p.check(dir=1) and (rec is None or rec(p))]
161
- )
162
- if not self.breadthfirst:
163
- for subdir in dirs:
164
- yield from self.gen(subdir)
165
- for p in self.optsort(entries):
166
- if self.fil is None or self.fil(p):
167
- yield p
168
- if self.breadthfirst:
169
- for subdir in dirs:
170
- yield from self.gen(subdir)
171
-
172
-
173
- class FNMatcher:
174
- def __init__(self, pattern):
175
- self.pattern = pattern
176
-
177
- def __call__(self, path):
178
- pattern = self.pattern
179
-
180
- if (
181
- pattern.find(path.sep) == -1
182
- and iswin32
183
- and pattern.find(posixpath.sep) != -1
184
- ):
185
- # Running on Windows, the pattern has no Windows path separators,
186
- # and the pattern has one or more Posix path separators. Replace
187
- # the Posix path separators with the Windows path separator.
188
- pattern = pattern.replace(posixpath.sep, path.sep)
189
-
190
- if pattern.find(path.sep) == -1:
191
- name = path.basename
192
- else:
193
- name = str(path) # path.strpath # XXX svn?
194
- if not os.path.isabs(pattern):
195
- pattern = "*" + path.sep + pattern
196
- return fnmatch.fnmatch(name, pattern)
197
-
198
-
199
- def map_as_list(func, iter):
200
- return list(map(func, iter))
201
-
202
-
203
- class Stat:
204
- if TYPE_CHECKING:
205
-
206
- @property
207
- def size(self) -> int: ...
208
-
209
- @property
210
- def mtime(self) -> float: ...
211
-
212
- def __getattr__(self, name: str) -> Any:
213
- return getattr(self._osstatresult, "st_" + name)
214
-
215
- def __init__(self, path, osstatresult):
216
- self.path = path
217
- self._osstatresult = osstatresult
218
-
219
- @property
220
- def owner(self):
221
- if iswin32:
222
- raise NotImplementedError("XXX win32")
223
- import pwd
224
-
225
- entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore]
226
- return entry[0]
227
-
228
- @property
229
- def group(self):
230
- """Return group name of file."""
231
- if iswin32:
232
- raise NotImplementedError("XXX win32")
233
- import grp
234
-
235
- entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore]
236
- return entry[0]
237
-
238
- def isdir(self):
239
- return S_ISDIR(self._osstatresult.st_mode)
240
-
241
- def isfile(self):
242
- return S_ISREG(self._osstatresult.st_mode)
243
-
244
- def islink(self):
245
- self.path.lstat()
246
- return S_ISLNK(self._osstatresult.st_mode)
247
-
248
-
249
- def getuserid(user):
250
- import pwd
251
-
252
- if not isinstance(user, int):
253
- user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore]
254
- return user
255
-
256
-
257
- def getgroupid(group):
258
- import grp
259
-
260
- if not isinstance(group, int):
261
- group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore]
262
- return group
263
-
264
-
265
- class LocalPath:
266
- """Object oriented interface to os.path and other local filesystem
267
- related information.
268
- """
269
-
270
- class ImportMismatchError(ImportError):
271
- """raised on pyimport() if there is a mismatch of __file__'s"""
272
-
273
- sep = os.sep
274
-
275
- def __init__(self, path=None, expanduser=False):
276
- """Initialize and return a local Path instance.
277
-
278
- Path can be relative to the current directory.
279
- If path is None it defaults to the current working directory.
280
- If expanduser is True, tilde-expansion is performed.
281
- Note that Path instances always carry an absolute path.
282
- Note also that passing in a local path object will simply return
283
- the exact same path object. Use new() to get a new copy.
284
- """
285
- if path is None:
286
- self.strpath = error.checked_call(os.getcwd)
287
- else:
288
- try:
289
- path = os.fspath(path)
290
- except TypeError:
291
- raise ValueError(
292
- "can only pass None, Path instances "
293
- "or non-empty strings to LocalPath"
294
- )
295
- if expanduser:
296
- path = os.path.expanduser(path)
297
- self.strpath = abspath(path)
298
-
299
- if sys.platform != "win32":
300
-
301
- def chown(self, user, group, rec=0):
302
- """Change ownership to the given user and group.
303
- user and group may be specified by a number or
304
- by a name. if rec is True change ownership
305
- recursively.
306
- """
307
- uid = getuserid(user)
308
- gid = getgroupid(group)
309
- if rec:
310
- for x in self.visit(rec=lambda x: x.check(link=0)):
311
- if x.check(link=0):
312
- error.checked_call(os.chown, str(x), uid, gid)
313
- error.checked_call(os.chown, str(self), uid, gid)
314
-
315
- def readlink(self) -> str:
316
- """Return value of a symbolic link."""
317
- # https://github.com/python/mypy/issues/12278
318
- return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore]
319
-
320
- def mklinkto(self, oldname):
321
- """Posix style hard link to another name."""
322
- error.checked_call(os.link, str(oldname), str(self))
323
-
324
- def mksymlinkto(self, value, absolute=1):
325
- """Create a symbolic link with the given value (pointing to another name)."""
326
- if absolute:
327
- error.checked_call(os.symlink, str(value), self.strpath)
328
- else:
329
- base = self.common(value)
330
- # with posix local paths '/' is always a common base
331
- relsource = self.__class__(value).relto(base)
332
- reldest = self.relto(base)
333
- n = reldest.count(self.sep)
334
- target = self.sep.join(("..",) * n + (relsource,))
335
- error.checked_call(os.symlink, target, self.strpath)
336
-
337
- def __div__(self, other):
338
- return self.join(os.fspath(other))
339
-
340
- __truediv__ = __div__ # py3k
341
-
342
- @property
343
- def basename(self):
344
- """Basename part of path."""
345
- return self._getbyspec("basename")[0]
346
-
347
- @property
348
- def dirname(self):
349
- """Dirname part of path."""
350
- return self._getbyspec("dirname")[0]
351
-
352
- @property
353
- def purebasename(self):
354
- """Pure base name of the path."""
355
- return self._getbyspec("purebasename")[0]
356
-
357
- @property
358
- def ext(self):
359
- """Extension of the path (including the '.')."""
360
- return self._getbyspec("ext")[0]
361
-
362
- def read_binary(self):
363
- """Read and return a bytestring from reading the path."""
364
- with self.open("rb") as f:
365
- return f.read()
366
-
367
- def read_text(self, encoding):
368
- """Read and return a Unicode string from reading the path."""
369
- with self.open("r", encoding=encoding) as f:
370
- return f.read()
371
-
372
- def read(self, mode="r"):
373
- """Read and return a bytestring from reading the path."""
374
- with self.open(mode) as f:
375
- return f.read()
376
-
377
- def readlines(self, cr=1):
378
- """Read and return a list of lines from the path. if cr is False, the
379
- newline will be removed from the end of each line."""
380
- mode = "r"
381
-
382
- if not cr:
383
- content = self.read(mode)
384
- return content.split("\n")
385
- else:
386
- f = self.open(mode)
387
- try:
388
- return f.readlines()
389
- finally:
390
- f.close()
391
-
392
- def load(self):
393
- """(deprecated) return object unpickled from self.read()"""
394
- f = self.open("rb")
395
- try:
396
- import pickle
397
-
398
- return error.checked_call(pickle.load, f)
399
- finally:
400
- f.close()
401
-
402
- def move(self, target):
403
- """Move this path to target."""
404
- if target.relto(self):
405
- raise error.EINVAL(target, "cannot move path into a subdirectory of itself")
406
- try:
407
- self.rename(target)
408
- except error.EXDEV: # invalid cross-device link
409
- self.copy(target)
410
- self.remove()
411
-
412
- def fnmatch(self, pattern):
413
- """Return true if the basename/fullname matches the glob-'pattern'.
414
-
415
- valid pattern characters::
416
-
417
- * matches everything
418
- ? matches any single character
419
- [seq] matches any character in seq
420
- [!seq] matches any char not in seq
421
-
422
- If the pattern contains a path-separator then the full path
423
- is used for pattern matching and a '*' is prepended to the
424
- pattern.
425
-
426
- if the pattern doesn't contain a path-separator the pattern
427
- is only matched against the basename.
428
- """
429
- return FNMatcher(pattern)(self)
430
-
431
- def relto(self, relpath):
432
- """Return a string which is the relative part of the path
433
- to the given 'relpath'.
434
- """
435
- if not isinstance(relpath, str | LocalPath):
436
- raise TypeError(f"{relpath!r}: not a string or path object")
437
- strrelpath = str(relpath)
438
- if strrelpath and strrelpath[-1] != self.sep:
439
- strrelpath += self.sep
440
- # assert strrelpath[-1] == self.sep
441
- # assert strrelpath[-2] != self.sep
442
- strself = self.strpath
443
- if sys.platform == "win32" or getattr(os, "_name", None) == "nt":
444
- if os.path.normcase(strself).startswith(os.path.normcase(strrelpath)):
445
- return strself[len(strrelpath) :]
446
- elif strself.startswith(strrelpath):
447
- return strself[len(strrelpath) :]
448
- return ""
449
-
450
- def ensure_dir(self, *args):
451
- """Ensure the path joined with args is a directory."""
452
- return self.ensure(*args, dir=True)
453
-
454
- def bestrelpath(self, dest):
455
- """Return a string which is a relative path from self
456
- (assumed to be a directory) to dest such that
457
- self.join(bestrelpath) == dest and if not such
458
- path can be determined return dest.
459
- """
460
- try:
461
- if self == dest:
462
- return os.curdir
463
- base = self.common(dest)
464
- if not base: # can be the case on windows
465
- return str(dest)
466
- self2base = self.relto(base)
467
- reldest = dest.relto(base)
468
- if self2base:
469
- n = self2base.count(self.sep) + 1
470
- else:
471
- n = 0
472
- lst = [os.pardir] * n
473
- if reldest:
474
- lst.append(reldest)
475
- target = dest.sep.join(lst)
476
- return target
477
- except AttributeError:
478
- return str(dest)
479
-
480
- def exists(self):
481
- return self.check()
482
-
483
- def isdir(self):
484
- return self.check(dir=1)
485
-
486
- def isfile(self):
487
- return self.check(file=1)
488
-
489
- def parts(self, reverse=False):
490
- """Return a root-first list of all ancestor directories
491
- plus the path itself.
492
- """
493
- current = self
494
- lst = [self]
495
- while 1:
496
- last = current
497
- current = current.dirpath()
498
- if last == current:
499
- break
500
- lst.append(current)
501
- if not reverse:
502
- lst.reverse()
503
- return lst
504
-
505
- def common(self, other):
506
- """Return the common part shared with the other path
507
- or None if there is no common part.
508
- """
509
- last = None
510
- for x, y in zip(self.parts(), other.parts()):
511
- if x != y:
512
- return last
513
- last = x
514
- return last
515
-
516
- def __add__(self, other):
517
- """Return new path object with 'other' added to the basename"""
518
- return self.new(basename=self.basename + str(other))
519
-
520
- def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False):
521
- """Yields all paths below the current one
522
-
523
- fil is a filter (glob pattern or callable), if not matching the
524
- path will not be yielded, defaulting to None (everything is
525
- returned)
526
-
527
- rec is a filter (glob pattern or callable) that controls whether
528
- a node is descended, defaulting to None
529
-
530
- ignore is an Exception class that is ignoredwhen calling dirlist()
531
- on any of the paths (by default, all exceptions are reported)
532
-
533
- bf if True will cause a breadthfirst search instead of the
534
- default depthfirst. Default: False
535
-
536
- sort if True will sort entries within each directory level.
537
- """
538
- yield from Visitor(fil, rec, ignore, bf, sort).gen(self)
539
-
540
- def _sortlist(self, res, sort):
541
- if sort:
542
- if hasattr(sort, "__call__"):
543
- warnings.warn(
544
- DeprecationWarning(
545
- "listdir(sort=callable) is deprecated and breaks on python3"
546
- ),
547
- stacklevel=3,
548
- )
549
- res.sort(sort)
550
- else:
551
- res.sort()
552
-
553
- def __fspath__(self):
554
- return self.strpath
555
-
556
- def __hash__(self):
557
- s = self.strpath
558
- if iswin32:
559
- s = s.lower()
560
- return hash(s)
561
-
562
- def __eq__(self, other):
563
- s1 = os.fspath(self)
564
- try:
565
- s2 = os.fspath(other)
566
- except TypeError:
567
- return False
568
- if iswin32:
569
- s1 = s1.lower()
570
- try:
571
- s2 = s2.lower()
572
- except AttributeError:
573
- return False
574
- return s1 == s2
575
-
576
- def __ne__(self, other):
577
- return not (self == other)
578
-
579
- def __lt__(self, other):
580
- return os.fspath(self) < os.fspath(other)
581
-
582
- def __gt__(self, other):
583
- return os.fspath(self) > os.fspath(other)
584
-
585
- def samefile(self, other):
586
- """Return True if 'other' references the same file as 'self'."""
587
- other = os.fspath(other)
588
- if not isabs(other):
589
- other = abspath(other)
590
- if self == other:
591
- return True
592
- if not hasattr(os.path, "samefile"):
593
- return False
594
- return error.checked_call(os.path.samefile, self.strpath, other)
595
-
596
- def remove(self, rec=1, ignore_errors=False):
597
- """Remove a file or directory (or a directory tree if rec=1).
598
- if ignore_errors is True, errors while removing directories will
599
- be ignored.
600
- """
601
- if self.check(dir=1, link=0):
602
- if rec:
603
- # force remove of readonly files on windows
604
- if iswin32:
605
- self.chmod(0o700, rec=1)
606
- import shutil
607
-
608
- error.checked_call(
609
- shutil.rmtree, self.strpath, ignore_errors=ignore_errors
610
- )
611
- else:
612
- error.checked_call(os.rmdir, self.strpath)
613
- else:
614
- if iswin32:
615
- self.chmod(0o700)
616
- error.checked_call(os.remove, self.strpath)
617
-
618
- def computehash(self, hashtype="md5", chunksize=524288):
619
- """Return hexdigest of hashvalue for this file."""
620
- try:
621
- try:
622
- import hashlib as mod
623
- except ImportError:
624
- if hashtype == "sha1":
625
- hashtype = "sha"
626
- mod = __import__(hashtype)
627
- hash = getattr(mod, hashtype)()
628
- except (AttributeError, ImportError):
629
- raise ValueError(f"Don't know how to compute {hashtype!r} hash")
630
- f = self.open("rb")
631
- try:
632
- while 1:
633
- buf = f.read(chunksize)
634
- if not buf:
635
- return hash.hexdigest()
636
- hash.update(buf)
637
- finally:
638
- f.close()
639
-
640
- def new(self, **kw):
641
- """Create a modified version of this path.
642
- the following keyword arguments modify various path parts::
643
-
644
- a:/some/path/to/a/file.ext
645
- xx drive
646
- xxxxxxxxxxxxxxxxx dirname
647
- xxxxxxxx basename
648
- xxxx purebasename
649
- xxx ext
650
- """
651
- obj = object.__new__(self.__class__)
652
- if not kw:
653
- obj.strpath = self.strpath
654
- return obj
655
- drive, dirname, _basename, purebasename, ext = self._getbyspec(
656
- "drive,dirname,basename,purebasename,ext"
657
- )
658
- if "basename" in kw:
659
- if "purebasename" in kw or "ext" in kw:
660
- raise ValueError(f"invalid specification {kw!r}")
661
- else:
662
- pb = kw.setdefault("purebasename", purebasename)
663
- try:
664
- ext = kw["ext"]
665
- except KeyError:
666
- pass
667
- else:
668
- if ext and not ext.startswith("."):
669
- ext = "." + ext
670
- kw["basename"] = pb + ext
671
-
672
- if "dirname" in kw and not kw["dirname"]:
673
- kw["dirname"] = drive
674
- else:
675
- kw.setdefault("dirname", dirname)
676
- kw.setdefault("sep", self.sep)
677
- obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw))
678
- return obj
679
-
680
- def _getbyspec(self, spec: str) -> list[str]:
681
- """See new for what 'spec' can be."""
682
- res = []
683
- parts = self.strpath.split(self.sep)
684
-
685
- args = filter(None, spec.split(","))
686
- for name in args:
687
- if name == "drive":
688
- res.append(parts[0])
689
- elif name == "dirname":
690
- res.append(self.sep.join(parts[:-1]))
691
- else:
692
- basename = parts[-1]
693
- if name == "basename":
694
- res.append(basename)
695
- else:
696
- i = basename.rfind(".")
697
- if i == -1:
698
- purebasename, ext = basename, ""
699
- else:
700
- purebasename, ext = basename[:i], basename[i:]
701
- if name == "purebasename":
702
- res.append(purebasename)
703
- elif name == "ext":
704
- res.append(ext)
705
- else:
706
- raise ValueError(f"invalid part specification {name!r}")
707
- return res
708
-
709
- def dirpath(self, *args, **kwargs):
710
- """Return the directory path joined with any given path arguments."""
711
- if not kwargs:
712
- path = object.__new__(self.__class__)
713
- path.strpath = dirname(self.strpath)
714
- if args:
715
- path = path.join(*args)
716
- return path
717
- return self.new(basename="").join(*args, **kwargs)
718
-
719
- def join(self, *args: os.PathLike[str], abs: bool = False) -> LocalPath:
720
- """Return a new path by appending all 'args' as path
721
- components. if abs=1 is used restart from root if any
722
- of the args is an absolute path.
723
- """
724
- sep = self.sep
725
- strargs = [os.fspath(arg) for arg in args]
726
- strpath = self.strpath
727
- if abs:
728
- newargs: list[str] = []
729
- for arg in reversed(strargs):
730
- if isabs(arg):
731
- strpath = arg
732
- strargs = newargs
733
- break
734
- newargs.insert(0, arg)
735
- # special case for when we have e.g. strpath == "/"
736
- actual_sep = "" if strpath.endswith(sep) else sep
737
- for arg in strargs:
738
- arg = arg.strip(sep)
739
- if iswin32:
740
- # allow unix style paths even on windows.
741
- arg = arg.strip("/")
742
- arg = arg.replace("/", sep)
743
- strpath = strpath + actual_sep + arg
744
- actual_sep = sep
745
- obj = object.__new__(self.__class__)
746
- obj.strpath = normpath(strpath)
747
- return obj
748
-
749
- def open(self, mode="r", ensure=False, encoding=None):
750
- """Return an opened file with the given mode.
751
-
752
- If ensure is True, create parent directories if needed.
753
- """
754
- if ensure:
755
- self.dirpath().ensure(dir=1)
756
- if encoding:
757
- return error.checked_call(
758
- io.open,
759
- self.strpath,
760
- mode,
761
- encoding=encoding,
762
- )
763
- return error.checked_call(open, self.strpath, mode)
764
-
765
- def _fastjoin(self, name):
766
- child = object.__new__(self.__class__)
767
- child.strpath = self.strpath + self.sep + name
768
- return child
769
-
770
- def islink(self):
771
- return islink(self.strpath)
772
-
773
- def check(self, **kw):
774
- """Check a path for existence and properties.
775
-
776
- Without arguments, return True if the path exists, otherwise False.
777
-
778
- valid checkers::
779
-
780
- file = 1 # is a file
781
- file = 0 # is not a file (may not even exist)
782
- dir = 1 # is a dir
783
- link = 1 # is a link
784
- exists = 1 # exists
785
-
786
- You can specify multiple checker definitions, for example::
787
-
788
- path.check(file=1, link=1) # a link pointing to a file
789
- """
790
- if not kw:
791
- return exists(self.strpath)
792
- if len(kw) == 1:
793
- if "dir" in kw:
794
- return not kw["dir"] ^ isdir(self.strpath)
795
- if "file" in kw:
796
- return not kw["file"] ^ isfile(self.strpath)
797
- if not kw:
798
- kw = {"exists": 1}
799
- return Checkers(self)._evaluate(kw)
800
-
801
- _patternchars = set("*?[" + os.sep)
802
-
803
- def listdir(self, fil=None, sort=None):
804
- """List directory contents, possibly filter by the given fil func
805
- and possibly sorted.
806
- """
807
- if fil is None and sort is None:
808
- names = error.checked_call(os.listdir, self.strpath)
809
- return map_as_list(self._fastjoin, names)
810
- if isinstance(fil, str):
811
- if not self._patternchars.intersection(fil):
812
- child = self._fastjoin(fil)
813
- if exists(child.strpath):
814
- return [child]
815
- return []
816
- fil = FNMatcher(fil)
817
- names = error.checked_call(os.listdir, self.strpath)
818
- res = []
819
- for name in names:
820
- child = self._fastjoin(name)
821
- if fil is None or fil(child):
822
- res.append(child)
823
- self._sortlist(res, sort)
824
- return res
825
-
826
- def size(self) -> int:
827
- """Return size of the underlying file object"""
828
- return self.stat().size
829
-
830
- def mtime(self) -> float:
831
- """Return last modification time of the path."""
832
- return self.stat().mtime
833
-
834
- def copy(self, target, mode=False, stat=False):
835
- """Copy path to target.
836
-
837
- If mode is True, will copy permission from path to target.
838
- If stat is True, copy permission, last modification
839
- time, last access time, and flags from path to target.
840
- """
841
- if self.check(file=1):
842
- if target.check(dir=1):
843
- target = target.join(self.basename)
844
- assert self != target
845
- copychunked(self, target)
846
- if mode:
847
- copymode(self.strpath, target.strpath)
848
- if stat:
849
- copystat(self, target)
850
- else:
851
-
852
- def rec(p):
853
- return p.check(link=0)
854
-
855
- for x in self.visit(rec=rec):
856
- relpath = x.relto(self)
857
- newx = target.join(relpath)
858
- newx.dirpath().ensure(dir=1)
859
- if x.check(link=1):
860
- newx.mksymlinkto(x.readlink())
861
- continue
862
- elif x.check(file=1):
863
- copychunked(x, newx)
864
- elif x.check(dir=1):
865
- newx.ensure(dir=1)
866
- if mode:
867
- copymode(x.strpath, newx.strpath)
868
- if stat:
869
- copystat(x, newx)
870
-
871
- def rename(self, target):
872
- """Rename this path to target."""
873
- target = os.fspath(target)
874
- return error.checked_call(os.rename, self.strpath, target)
875
-
876
- def dump(self, obj, bin=1):
877
- """Pickle object into path location"""
878
- f = self.open("wb")
879
- import pickle
880
-
881
- try:
882
- error.checked_call(pickle.dump, obj, f, bin)
883
- finally:
884
- f.close()
885
-
886
- def mkdir(self, *args):
887
- """Create & return the directory joined with args."""
888
- p = self.join(*args)
889
- error.checked_call(os.mkdir, os.fspath(p))
890
- return p
891
-
892
- def write_binary(self, data, ensure=False):
893
- """Write binary data into path. If ensure is True create
894
- missing parent directories.
895
- """
896
- if ensure:
897
- self.dirpath().ensure(dir=1)
898
- with self.open("wb") as f:
899
- f.write(data)
900
-
901
- def write_text(self, data, encoding, ensure=False):
902
- """Write text data into path using the specified encoding.
903
- If ensure is True create missing parent directories.
904
- """
905
- if ensure:
906
- self.dirpath().ensure(dir=1)
907
- with self.open("w", encoding=encoding) as f:
908
- f.write(data)
909
-
910
- def write(self, data, mode="w", ensure=False):
911
- """Write data into path. If ensure is True create
912
- missing parent directories.
913
- """
914
- if ensure:
915
- self.dirpath().ensure(dir=1)
916
- if "b" in mode:
917
- if not isinstance(data, bytes):
918
- raise ValueError("can only process bytes")
919
- else:
920
- if not isinstance(data, str):
921
- if not isinstance(data, bytes):
922
- data = str(data)
923
- else:
924
- data = data.decode(sys.getdefaultencoding())
925
- f = self.open(mode)
926
- try:
927
- f.write(data)
928
- finally:
929
- f.close()
930
-
931
- def _ensuredirs(self):
932
- parent = self.dirpath()
933
- if parent == self:
934
- return self
935
- if parent.check(dir=0):
936
- parent._ensuredirs()
937
- if self.check(dir=0):
938
- try:
939
- self.mkdir()
940
- except error.EEXIST:
941
- # race condition: file/dir created by another thread/process.
942
- # complain if it is not a dir
943
- if self.check(dir=0):
944
- raise
945
- return self
946
-
947
- def ensure(self, *args, **kwargs):
948
- """Ensure that an args-joined path exists (by default as
949
- a file). if you specify a keyword argument 'dir=True'
950
- then the path is forced to be a directory path.
951
- """
952
- p = self.join(*args)
953
- if kwargs.get("dir", 0):
954
- return p._ensuredirs()
955
- else:
956
- p.dirpath()._ensuredirs()
957
- if not p.check(file=1):
958
- p.open("wb").close()
959
- return p
960
-
961
- @overload
962
- def stat(self, raising: Literal[True] = ...) -> Stat: ...
963
-
964
- @overload
965
- def stat(self, raising: Literal[False]) -> Stat | None: ...
966
-
967
- def stat(self, raising: bool = True) -> Stat | None:
968
- """Return an os.stat() tuple."""
969
- if raising:
970
- return Stat(self, error.checked_call(os.stat, self.strpath))
971
- try:
972
- return Stat(self, os.stat(self.strpath))
973
- except KeyboardInterrupt:
974
- raise
975
- except Exception:
976
- return None
977
-
978
- def lstat(self) -> Stat:
979
- """Return an os.lstat() tuple."""
980
- return Stat(self, error.checked_call(os.lstat, self.strpath))
981
-
982
- def setmtime(self, mtime=None):
983
- """Set modification time for the given path. if 'mtime' is None
984
- (the default) then the file's mtime is set to current time.
985
-
986
- Note that the resolution for 'mtime' is platform dependent.
987
- """
988
- if mtime is None:
989
- return error.checked_call(os.utime, self.strpath, mtime)
990
- try:
991
- return error.checked_call(os.utime, self.strpath, (-1, mtime))
992
- except error.EINVAL:
993
- return error.checked_call(os.utime, self.strpath, (self.atime(), mtime))
994
-
995
- def chdir(self):
996
- """Change directory to self and return old current directory"""
997
- try:
998
- old = self.__class__()
999
- except error.ENOENT:
1000
- old = None
1001
- error.checked_call(os.chdir, self.strpath)
1002
- return old
1003
-
1004
- @contextmanager
1005
- def as_cwd(self):
1006
- """
1007
- Return a context manager, which changes to the path's dir during the
1008
- managed "with" context.
1009
- On __enter__ it returns the old dir, which might be ``None``.
1010
- """
1011
- old = self.chdir()
1012
- try:
1013
- yield old
1014
- finally:
1015
- if old is not None:
1016
- old.chdir()
1017
-
1018
- def realpath(self):
1019
- """Return a new path which contains no symbolic links."""
1020
- return self.__class__(os.path.realpath(self.strpath))
1021
-
1022
- def atime(self):
1023
- """Return last access time of the path."""
1024
- return self.stat().atime
1025
-
1026
- def __repr__(self):
1027
- return f"local({self.strpath!r})"
1028
-
1029
- def __str__(self):
1030
- """Return string representation of the Path."""
1031
- return self.strpath
1032
-
1033
- def chmod(self, mode, rec=0):
1034
- """Change permissions to the given mode. If mode is an
1035
- integer it directly encodes the os-specific modes.
1036
- if rec is True perform recursively.
1037
- """
1038
- if not isinstance(mode, int):
1039
- raise TypeError(f"mode {mode!r} must be an integer")
1040
- if rec:
1041
- for x in self.visit(rec=rec):
1042
- error.checked_call(os.chmod, str(x), mode)
1043
- error.checked_call(os.chmod, self.strpath, mode)
1044
-
1045
- def pypkgpath(self):
1046
- """Return the Python package path by looking for the last
1047
- directory upwards which still contains an __init__.py.
1048
- Return None if a pkgpath cannot be determined.
1049
- """
1050
- pkgpath = None
1051
- for parent in self.parts(reverse=True):
1052
- if parent.isdir():
1053
- if not parent.join("__init__.py").exists():
1054
- break
1055
- if not isimportable(parent.basename):
1056
- break
1057
- pkgpath = parent
1058
- return pkgpath
1059
-
1060
- def _ensuresyspath(self, ensuremode, path):
1061
- if ensuremode:
1062
- s = str(path)
1063
- if ensuremode == "append":
1064
- if s not in sys.path:
1065
- sys.path.append(s)
1066
- else:
1067
- if s != sys.path[0]:
1068
- sys.path.insert(0, s)
1069
-
1070
- def pyimport(self, modname=None, ensuresyspath=True):
1071
- """Return path as an imported python module.
1072
-
1073
- If modname is None, look for the containing package
1074
- and construct an according module name.
1075
- The module will be put/looked up in sys.modules.
1076
- if ensuresyspath is True then the root dir for importing
1077
- the file (taking __init__.py files into account) will
1078
- be prepended to sys.path if it isn't there already.
1079
- If ensuresyspath=="append" the root dir will be appended
1080
- if it isn't already contained in sys.path.
1081
- if ensuresyspath is False no modification of syspath happens.
1082
-
1083
- Special value of ensuresyspath=="importlib" is intended
1084
- purely for using in pytest, it is capable only of importing
1085
- separate .py files outside packages, e.g. for test suite
1086
- without any __init__.py file. It effectively allows having
1087
- same-named test modules in different places and offers
1088
- mild opt-in via this option. Note that it works only in
1089
- recent versions of python.
1090
- """
1091
- if not self.check():
1092
- raise error.ENOENT(self)
1093
-
1094
- if ensuresyspath == "importlib":
1095
- if modname is None:
1096
- modname = self.purebasename
1097
- spec = importlib.util.spec_from_file_location(modname, str(self))
1098
- if spec is None or spec.loader is None:
1099
- raise ImportError(f"Can't find module {modname} at location {self!s}")
1100
- mod = importlib.util.module_from_spec(spec)
1101
- spec.loader.exec_module(mod)
1102
- return mod
1103
-
1104
- pkgpath = None
1105
- if modname is None:
1106
- pkgpath = self.pypkgpath()
1107
- if pkgpath is not None:
1108
- pkgroot = pkgpath.dirpath()
1109
- names = self.new(ext="").relto(pkgroot).split(self.sep)
1110
- if names[-1] == "__init__":
1111
- names.pop()
1112
- modname = ".".join(names)
1113
- else:
1114
- pkgroot = self.dirpath()
1115
- modname = self.purebasename
1116
-
1117
- self._ensuresyspath(ensuresyspath, pkgroot)
1118
- __import__(modname)
1119
- mod = sys.modules[modname]
1120
- if self.basename == "__init__.py":
1121
- return mod # we don't check anything as we might
1122
- # be in a namespace package ... too icky to check
1123
- modfile = mod.__file__
1124
- assert modfile is not None
1125
- if modfile[-4:] in (".pyc", ".pyo"):
1126
- modfile = modfile[:-1]
1127
- elif modfile.endswith("$py.class"):
1128
- modfile = modfile[:-9] + ".py"
1129
- if modfile.endswith(os.sep + "__init__.py"):
1130
- if self.basename != "__init__.py":
1131
- modfile = modfile[:-12]
1132
- try:
1133
- issame = self.samefile(modfile)
1134
- except error.ENOENT:
1135
- issame = False
1136
- if not issame:
1137
- ignore = os.getenv("PY_IGNORE_IMPORTMISMATCH")
1138
- if ignore != "1":
1139
- raise self.ImportMismatchError(modname, modfile, self)
1140
- return mod
1141
- else:
1142
- try:
1143
- return sys.modules[modname]
1144
- except KeyError:
1145
- # we have a custom modname, do a pseudo-import
1146
- import types
1147
-
1148
- mod = types.ModuleType(modname)
1149
- mod.__file__ = str(self)
1150
- sys.modules[modname] = mod
1151
- try:
1152
- with open(str(self), "rb") as f:
1153
- exec(f.read(), mod.__dict__)
1154
- except BaseException:
1155
- del sys.modules[modname]
1156
- raise
1157
- return mod
1158
-
1159
- def sysexec(self, *argv: os.PathLike[str], **popen_opts: Any) -> str:
1160
- """Return stdout text from executing a system child process,
1161
- where the 'self' path points to executable.
1162
- The process is directly invoked and not through a system shell.
1163
- """
1164
- from subprocess import PIPE
1165
- from subprocess import Popen
1166
-
1167
- popen_opts.pop("stdout", None)
1168
- popen_opts.pop("stderr", None)
1169
- proc = Popen(
1170
- [str(self)] + [str(arg) for arg in argv],
1171
- **popen_opts,
1172
- stdout=PIPE,
1173
- stderr=PIPE,
1174
- )
1175
- stdout: str | bytes
1176
- stdout, stderr = proc.communicate()
1177
- ret = proc.wait()
1178
- if isinstance(stdout, bytes):
1179
- stdout = stdout.decode(sys.getdefaultencoding())
1180
- if ret != 0:
1181
- if isinstance(stderr, bytes):
1182
- stderr = stderr.decode(sys.getdefaultencoding())
1183
- raise RuntimeError(
1184
- ret,
1185
- ret,
1186
- str(self),
1187
- stdout,
1188
- stderr,
1189
- )
1190
- return stdout
1191
-
1192
- @classmethod
1193
- def sysfind(cls, name, checker=None, paths=None):
1194
- """Return a path object found by looking at the systems
1195
- underlying PATH specification. If the checker is not None
1196
- it will be invoked to filter matching paths. If a binary
1197
- cannot be found, None is returned
1198
- Note: This is probably not working on plain win32 systems
1199
- but may work on cygwin.
1200
- """
1201
- if isabs(name):
1202
- p = local(name)
1203
- if p.check(file=1):
1204
- return p
1205
- else:
1206
- if paths is None:
1207
- if iswin32:
1208
- paths = os.environ["Path"].split(";")
1209
- if "" not in paths and "." not in paths:
1210
- paths.append(".")
1211
- try:
1212
- systemroot = os.environ["SYSTEMROOT"]
1213
- except KeyError:
1214
- pass
1215
- else:
1216
- paths = [
1217
- path.replace("%SystemRoot%", systemroot) for path in paths
1218
- ]
1219
- else:
1220
- paths = os.environ["PATH"].split(":")
1221
- tryadd = []
1222
- if iswin32:
1223
- tryadd += os.environ["PATHEXT"].split(os.pathsep)
1224
- tryadd.append("")
1225
-
1226
- for x in paths:
1227
- for addext in tryadd:
1228
- p = local(x).join(name, abs=True) + addext
1229
- try:
1230
- if p.check(file=1):
1231
- if checker:
1232
- if not checker(p):
1233
- continue
1234
- return p
1235
- except error.EACCES:
1236
- pass
1237
- return None
1238
-
1239
- @classmethod
1240
- def _gethomedir(cls):
1241
- try:
1242
- x = os.environ["HOME"]
1243
- except KeyError:
1244
- try:
1245
- x = os.environ["HOMEDRIVE"] + os.environ["HOMEPATH"]
1246
- except KeyError:
1247
- return None
1248
- return cls(x)
1249
-
1250
- # """
1251
- # special class constructors for local filesystem paths
1252
- # """
1253
- @classmethod
1254
- def get_temproot(cls):
1255
- """Return the system's temporary directory
1256
- (where tempfiles are usually created in)
1257
- """
1258
- import tempfile
1259
-
1260
- return local(tempfile.gettempdir())
1261
-
1262
- @classmethod
1263
- def mkdtemp(cls, rootdir=None):
1264
- """Return a Path object pointing to a fresh new temporary directory
1265
- (which we created ourselves).
1266
- """
1267
- import tempfile
1268
-
1269
- if rootdir is None:
1270
- rootdir = cls.get_temproot()
1271
- path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir))
1272
- return cls(path)
1273
-
1274
- @classmethod
1275
- def make_numbered_dir(
1276
- cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800
1277
- ): # two days
1278
- """Return unique directory with a number greater than the current
1279
- maximum one. The number is assumed to start directly after prefix.
1280
- if keep is true directories with a number less than (maxnum-keep)
1281
- will be removed. If .lock files are used (lock_timeout non-zero),
1282
- algorithm is multi-process safe.
1283
- """
1284
- if rootdir is None:
1285
- rootdir = cls.get_temproot()
1286
-
1287
- nprefix = prefix.lower()
1288
-
1289
- def parse_num(path):
1290
- """Parse the number out of a path (if it matches the prefix)"""
1291
- nbasename = path.basename.lower()
1292
- if nbasename.startswith(nprefix):
1293
- try:
1294
- return int(nbasename[len(nprefix) :])
1295
- except ValueError:
1296
- pass
1297
-
1298
- def create_lockfile(path):
1299
- """Exclusively create lockfile. Throws when failed"""
1300
- mypid = os.getpid()
1301
- lockfile = path.join(".lock")
1302
- if hasattr(lockfile, "mksymlinkto"):
1303
- lockfile.mksymlinkto(str(mypid))
1304
- else:
1305
- fd = error.checked_call(
1306
- os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644
1307
- )
1308
- with os.fdopen(fd, "w") as f:
1309
- f.write(str(mypid))
1310
- return lockfile
1311
-
1312
- def atexit_remove_lockfile(lockfile):
1313
- """Ensure lockfile is removed at process exit"""
1314
- mypid = os.getpid()
1315
-
1316
- def try_remove_lockfile():
1317
- # in a fork() situation, only the last process should
1318
- # remove the .lock, otherwise the other processes run the
1319
- # risk of seeing their temporary dir disappear. For now
1320
- # we remove the .lock in the parent only (i.e. we assume
1321
- # that the children finish before the parent).
1322
- if os.getpid() != mypid:
1323
- return
1324
- try:
1325
- lockfile.remove()
1326
- except error.Error:
1327
- pass
1328
-
1329
- atexit.register(try_remove_lockfile)
1330
-
1331
- # compute the maximum number currently in use with the prefix
1332
- lastmax = None
1333
- while True:
1334
- maxnum = -1
1335
- for path in rootdir.listdir():
1336
- num = parse_num(path)
1337
- if num is not None:
1338
- maxnum = max(maxnum, num)
1339
-
1340
- # make the new directory
1341
- try:
1342
- udir = rootdir.mkdir(prefix + str(maxnum + 1))
1343
- if lock_timeout:
1344
- lockfile = create_lockfile(udir)
1345
- atexit_remove_lockfile(lockfile)
1346
- except (error.EEXIST, error.ENOENT, error.EBUSY):
1347
- # race condition (1): another thread/process created the dir
1348
- # in the meantime - try again
1349
- # race condition (2): another thread/process spuriously acquired
1350
- # lock treating empty directory as candidate
1351
- # for removal - try again
1352
- # race condition (3): another thread/process tried to create the lock at
1353
- # the same time (happened in Python 3.3 on Windows)
1354
- # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa
1355
- if lastmax == maxnum:
1356
- raise
1357
- lastmax = maxnum
1358
- continue
1359
- break
1360
-
1361
- def get_mtime(path):
1362
- """Read file modification time"""
1363
- try:
1364
- return path.lstat().mtime
1365
- except error.Error:
1366
- pass
1367
-
1368
- garbage_prefix = prefix + "garbage-"
1369
-
1370
- def is_garbage(path):
1371
- """Check if path denotes directory scheduled for removal"""
1372
- bn = path.basename
1373
- return bn.startswith(garbage_prefix)
1374
-
1375
- # prune old directories
1376
- udir_time = get_mtime(udir)
1377
- if keep and udir_time:
1378
- for path in rootdir.listdir():
1379
- num = parse_num(path)
1380
- if num is not None and num <= (maxnum - keep):
1381
- try:
1382
- # try acquiring lock to remove directory as exclusive user
1383
- if lock_timeout:
1384
- create_lockfile(path)
1385
- except (error.EEXIST, error.ENOENT, error.EBUSY):
1386
- path_time = get_mtime(path)
1387
- if not path_time:
1388
- # assume directory doesn't exist now
1389
- continue
1390
- if abs(udir_time - path_time) < lock_timeout:
1391
- # assume directory with lockfile exists
1392
- # and lock timeout hasn't expired yet
1393
- continue
1394
-
1395
- # path dir locked for exclusive use
1396
- # and scheduled for removal to avoid another thread/process
1397
- # treating it as a new directory or removal candidate
1398
- garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4()))
1399
- try:
1400
- path.rename(garbage_path)
1401
- garbage_path.remove(rec=1)
1402
- except KeyboardInterrupt:
1403
- raise
1404
- except Exception: # this might be error.Error, WindowsError ...
1405
- pass
1406
- if is_garbage(path):
1407
- try:
1408
- path.remove(rec=1)
1409
- except KeyboardInterrupt:
1410
- raise
1411
- except Exception: # this might be error.Error, WindowsError ...
1412
- pass
1413
-
1414
- # make link...
1415
- try:
1416
- username = os.environ["USER"] # linux, et al
1417
- except KeyError:
1418
- try:
1419
- username = os.environ["USERNAME"] # windows
1420
- except KeyError:
1421
- username = "current"
1422
-
1423
- src = str(udir)
1424
- dest = src[: src.rfind("-")] + "-" + username
1425
- try:
1426
- os.unlink(dest)
1427
- except OSError:
1428
- pass
1429
- try:
1430
- os.symlink(src, dest)
1431
- except (OSError, AttributeError, NotImplementedError):
1432
- pass
1433
-
1434
- return udir
1435
-
1436
-
1437
- def copymode(src, dest):
1438
- """Copy permission from src to dst."""
1439
- import shutil
1440
-
1441
- shutil.copymode(src, dest)
1442
-
1443
-
1444
- def copystat(src, dest):
1445
- """Copy permission, last modification time,
1446
- last access time, and flags from src to dst."""
1447
- import shutil
1448
-
1449
- shutil.copystat(str(src), str(dest))
1450
-
1451
-
1452
- def copychunked(src, dest):
1453
- chunksize = 524288 # half a meg of bytes
1454
- fsrc = src.open("rb")
1455
- try:
1456
- fdest = dest.open("wb")
1457
- try:
1458
- while 1:
1459
- buf = fsrc.read(chunksize)
1460
- if not buf:
1461
- break
1462
- fdest.write(buf)
1463
- finally:
1464
- fdest.close()
1465
- finally:
1466
- fsrc.close()
1467
-
1468
-
1469
- def isimportable(name):
1470
- if name and (name[0].isalpha() or name[0] == "_"):
1471
- name = name.replace("_", "")
1472
- return not name or name.isalnum()
1473
-
1474
-
1475
- local = LocalPath
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/_version.py DELETED
@@ -1,24 +0,0 @@
1
- # file generated by vcs-versioning
2
- # don't change, don't track in version control
3
- from __future__ import annotations
4
-
5
- __all__ = [
6
- "__version__",
7
- "__version_tuple__",
8
- "version",
9
- "version_tuple",
10
- "__commit_id__",
11
- "commit_id",
12
- ]
13
-
14
- version: str
15
- __version__: str
16
- __version_tuple__: tuple[int | str, ...]
17
- version_tuple: tuple[int | str, ...]
18
- commit_id: str | None
19
- __commit_id__: str | None
20
-
21
- __version__ = version = '9.1.1'
22
- __version_tuple__ = version_tuple = (9, 1, 1)
23
-
24
- __commit_id__ = commit_id = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/__init__.py DELETED
@@ -1,236 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- """Support for presenting detailed information in failing assertions."""
3
-
4
- from __future__ import annotations
5
-
6
- from collections.abc import Generator
7
- import sys
8
- from typing import Any
9
- from typing import Protocol
10
- from typing import TYPE_CHECKING
11
-
12
- from _pytest.assertion import rewrite
13
- from _pytest.assertion import truncate
14
- from _pytest.assertion import util
15
- from _pytest.assertion.rewrite import assertstate_key
16
- from _pytest.config import Config
17
- from _pytest.config import hookimpl
18
- from _pytest.config.argparsing import Parser
19
- from _pytest.nodes import Item
20
-
21
-
22
- if TYPE_CHECKING:
23
- from _pytest.main import Session
24
-
25
-
26
- def pytest_addoption(parser: Parser) -> None:
27
- group = parser.getgroup("debugconfig")
28
- group.addoption(
29
- "--assert",
30
- action="store",
31
- dest="assertmode",
32
- choices=("rewrite", "plain"),
33
- default="rewrite",
34
- metavar="MODE",
35
- help=(
36
- "Control assertion debugging tools.\n"
37
- "'plain' performs no assertion debugging.\n"
38
- "'rewrite' (the default) rewrites assert statements in test modules"
39
- " on import to provide assert expression information."
40
- ),
41
- )
42
- parser.addini(
43
- "enable_assertion_pass_hook",
44
- type="bool",
45
- default=False,
46
- help="Enables the pytest_assertion_pass hook. "
47
- "Make sure to delete any previously generated pyc cache files.",
48
- )
49
-
50
- parser.addini(
51
- "truncation_limit_lines",
52
- default=None,
53
- help="Set threshold of LINES after which truncation will take effect",
54
- )
55
- parser.addini(
56
- "truncation_limit_chars",
57
- default=None,
58
- help=("Set threshold of CHARS after which truncation will take effect"),
59
- )
60
- parser.addini(
61
- "assertion_text_diff_style",
62
- default=util.ASSERTION_TEXT_DIFF_STYLE_NDIFF,
63
- help=(
64
- "Choose how pytest renders diffs for string equality assertions: "
65
- f"{util.ASSERTION_TEXT_DIFF_STYLE_NDIFF} or "
66
- f"{util.ASSERTION_TEXT_DIFF_STYLE_BLOCK}"
67
- ),
68
- )
69
-
70
- Config._add_verbosity_ini(
71
- parser,
72
- Config.VERBOSITY_ASSERTIONS,
73
- help=(
74
- "Specify a verbosity level for assertions, overriding the main level. "
75
- "Higher levels will provide more detailed explanation when an assertion fails."
76
- ),
77
- )
78
-
79
-
80
- def pytest_configure(config: Config) -> None:
81
- util.validate_assertion_text_diff_style(config)
82
-
83
-
84
- def register_assert_rewrite(*names: str) -> None:
85
- """Register one or more module names to be rewritten on import.
86
-
87
- This function will make sure that this module or all modules inside
88
- the package will get their assert statements rewritten.
89
- Thus you should make sure to call this before the module is
90
- actually imported, usually in your __init__.py if you are a plugin
91
- using a package.
92
-
93
- :param names: The module names to register.
94
- """
95
- for name in names:
96
- if not isinstance(name, str):
97
- msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable]
98
- raise TypeError(msg.format(repr(names)))
99
- rewrite_hook: RewriteHook
100
- for hook in sys.meta_path:
101
- if isinstance(hook, rewrite.AssertionRewritingHook):
102
- rewrite_hook = hook
103
- break
104
- else:
105
- rewrite_hook = DummyRewriteHook()
106
- rewrite_hook.mark_rewrite(*names)
107
-
108
-
109
- class RewriteHook(Protocol):
110
- def mark_rewrite(self, *names: str) -> None: ...
111
-
112
-
113
- class DummyRewriteHook:
114
- """A no-op import hook for when rewriting is disabled."""
115
-
116
- def mark_rewrite(self, *names: str) -> None:
117
- pass
118
-
119
-
120
- class AssertionState:
121
- """State for the assertion plugin."""
122
-
123
- def __init__(self, config: Config, mode) -> None:
124
- self.mode = mode
125
- self.trace = config.trace.root.get("assertion")
126
- self.hook: rewrite.AssertionRewritingHook | None = None
127
-
128
-
129
- def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
130
- """Try to install the rewrite hook, raise SystemError if it fails."""
131
- config.stash[assertstate_key] = AssertionState(config, "rewrite")
132
- config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
133
- sys.meta_path.insert(0, hook)
134
- config.stash[assertstate_key].trace("installed rewrite import hook")
135
-
136
- def undo() -> None:
137
- hook = config.stash[assertstate_key].hook
138
- if hook is not None and hook in sys.meta_path:
139
- sys.meta_path.remove(hook)
140
-
141
- config.add_cleanup(undo)
142
- return hook
143
-
144
-
145
- def pytest_collection(session: Session) -> None:
146
- # This hook is only called when test modules are collected
147
- # so for example not in the managing process of pytest-xdist
148
- # (which does not collect test modules).
149
- assertstate = session.config.stash.get(assertstate_key, None)
150
- if assertstate:
151
- if assertstate.hook is not None:
152
- assertstate.hook.set_session(session)
153
-
154
-
155
- @hookimpl(wrapper=True, tryfirst=True)
156
- def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
157
- """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
158
-
159
- The rewrite module will use util._reprcompare if it exists to use custom
160
- reporting via the pytest_assertrepr_compare hook. This sets up this custom
161
- comparison for the test.
162
- """
163
- ihook = item.ihook
164
-
165
- def callbinrepr(op, left: object, right: object) -> str | None:
166
- """Call the pytest_assertrepr_compare hook and prepare the result.
167
-
168
- This uses the first result from the hook and then ensures the
169
- following:
170
- * Overly verbose explanations are truncated unless configured otherwise
171
- (eg. if running in verbose mode).
172
- * Embedded newlines are escaped to help util.format_explanation()
173
- later.
174
- * If the rewrite mode is used embedded %-characters are replaced
175
- to protect later % formatting.
176
-
177
- The result can be formatted by util.format_explanation() for
178
- pretty printing.
179
- """
180
- hook_result = ihook.pytest_assertrepr_compare(
181
- config=item.config, op=op, left=left, right=right
182
- )
183
- for new_expl in hook_result:
184
- if new_expl:
185
- new_expl = truncate.truncate_if_required(new_expl, item)
186
- new_expl = [line.replace("\n", "\\n") for line in new_expl]
187
- res = "\n~".join(new_expl)
188
- if item.config.getvalue("assertmode") == "rewrite":
189
- res = res.replace("%", "%%")
190
- return res
191
- return None
192
-
193
- saved_assert_hooks = util._reprcompare, util._assertion_pass
194
- util._reprcompare = callbinrepr
195
- util._config = item.config
196
-
197
- if ihook.pytest_assertion_pass.get_hookimpls():
198
-
199
- def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
200
- ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl)
201
-
202
- util._assertion_pass = call_assertion_pass_hook
203
-
204
- try:
205
- return (yield)
206
- finally:
207
- util._reprcompare, util._assertion_pass = saved_assert_hooks
208
- util._config = None
209
-
210
-
211
- def pytest_sessionfinish(session: Session) -> None:
212
- assertstate = session.config.stash.get(assertstate_key, None)
213
- if assertstate:
214
- if assertstate.hook is not None:
215
- assertstate.hook.set_session(None)
216
-
217
-
218
- def pytest_assertrepr_compare(
219
- config: Config, op: str, left: Any, right: Any
220
- ) -> list[str] | None:
221
- if config.pluginmanager.has_plugin("terminalreporter"):
222
- highlighter = config.get_terminal_writer()._highlight
223
- else:
224
- # Keep it plaintext when not using terminalrepoterer (#14377).
225
- highlighter = util.dummy_highlighter
226
- explanation = list(
227
- util.assertrepr_compare(
228
- op=op,
229
- left=left,
230
- right=right,
231
- verbose=config.get_verbosity(Config.VERBOSITY_ASSERTIONS),
232
- highlighter=highlighter,
233
- assertion_text_diff_style=util.get_assertion_text_diff_style(config),
234
- )
235
- )
236
- return explanation or None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/_compare_any.py DELETED
@@ -1,131 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Iterator
4
- import dataclasses
5
- import pprint
6
-
7
- from _pytest.assertion._compare_mapping import _compare_eq_mapping
8
- from _pytest.assertion._compare_sequence import _compare_eq_iterable
9
- from _pytest.assertion._compare_sequence import _compare_eq_sequence
10
- from _pytest.assertion._compare_set import _compare_eq_set
11
- from _pytest.assertion._guards import has_default_eq
12
- from _pytest.assertion._guards import isattrs
13
- from _pytest.assertion._guards import isdatacls
14
- from _pytest.assertion._guards import isiterable
15
- from _pytest.assertion._guards import ismapping
16
- from _pytest.assertion._guards import isnamedtuple
17
- from _pytest.assertion._guards import issequence
18
- from _pytest.assertion._guards import isset
19
- from _pytest.assertion._guards import istext
20
- from _pytest.assertion._typing import _AssertionTextDiffStyle
21
- from _pytest.assertion._typing import _HighlightFunc
22
- from _pytest.assertion.compare_text import _compare_eq_text
23
-
24
-
25
- def _compare_eq_any(
26
- left: object,
27
- right: object,
28
- highlighter: _HighlightFunc,
29
- verbose: int,
30
- assertion_text_diff_style: _AssertionTextDiffStyle,
31
- ) -> Iterator[str]:
32
- """Yield the per-line explanation for ``left == right`` (without summary).
33
-
34
- Yields nothing when no specialised explanation applies, so consumers
35
- can stream the output and bail out early (e.g. for truncation) without
36
- materialising the entire diff first.
37
- """
38
- if istext(left) and istext(right):
39
- yield from _compare_eq_text(
40
- left,
41
- right,
42
- highlighter,
43
- verbose,
44
- assertion_text_diff_style,
45
- )
46
- else:
47
- from _pytest.python_api import ApproxBase
48
-
49
- # Although the common order should be obtained == approx(...), allow both ways.
50
- if isinstance(right, ApproxBase):
51
- yield from right._repr_compare(left)
52
- elif isinstance(left, ApproxBase):
53
- yield from left._repr_compare(right)
54
- elif type(left) is type(right) and (
55
- isdatacls(left) or isattrs(left) or isnamedtuple(left)
56
- ):
57
- # Note: unlike dataclasses/attrs, namedtuples compare only the
58
- # field values, not the type or field names. But this branch
59
- # intentionally only handles the same-type case, which was often
60
- # used in older code bases before dataclasses/attrs were available.
61
- yield from _compare_eq_cls(
62
- left,
63
- right,
64
- highlighter,
65
- verbose,
66
- assertion_text_diff_style,
67
- )
68
- elif issequence(left) and issequence(right):
69
- yield from _compare_eq_sequence(left, right, highlighter, verbose)
70
- elif isset(left) and isset(right):
71
- yield from _compare_eq_set(left, right, highlighter, verbose)
72
- elif ismapping(left) and ismapping(right):
73
- yield from _compare_eq_mapping(left, right, highlighter, verbose)
74
-
75
- if isiterable(left) and isiterable(right):
76
- yield from _compare_eq_iterable(left, right, highlighter, verbose)
77
-
78
-
79
- def _compare_eq_cls(
80
- left: object,
81
- right: object,
82
- highlighter: _HighlightFunc,
83
- verbose: int,
84
- assertion_text_diff_style: _AssertionTextDiffStyle,
85
- ) -> Iterator[str]:
86
- if not has_default_eq(left):
87
- return
88
- if isdatacls(left):
89
- all_fields = dataclasses.fields(left)
90
- fields_to_check = [info.name for info in all_fields if info.compare]
91
- elif isattrs(left):
92
- all_fields = left.__attrs_attrs__ # type: ignore[attr-defined]
93
- fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
94
- elif isnamedtuple(left):
95
- fields_to_check = left._fields # type: ignore[attr-defined]
96
- else:
97
- assert False
98
-
99
- indent = " "
100
- same = []
101
- diff = []
102
- for field in fields_to_check:
103
- if getattr(left, field) == getattr(right, field):
104
- same.append(field)
105
- else:
106
- diff.append(field)
107
-
108
- if same or diff:
109
- yield ""
110
- if same and verbose < 2:
111
- yield f"Omitting {len(same)} identical items, use -vv to show"
112
- elif same:
113
- yield "Matching attributes:"
114
- yield from highlighter(pprint.pformat(same)).splitlines()
115
- if diff:
116
- yield "Differing attributes:"
117
- yield from highlighter(pprint.pformat(diff)).splitlines()
118
- for field in diff:
119
- field_left = getattr(left, field)
120
- field_right = getattr(right, field)
121
- yield ""
122
- yield f"Drill down into differing attribute {field}:"
123
- yield f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}"
124
- for line in _compare_eq_any(
125
- field_left,
126
- field_right,
127
- highlighter,
128
- verbose,
129
- assertion_text_diff_style,
130
- ):
131
- yield indent + line
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/_compare_mapping.py DELETED
@@ -1,48 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Iterator
4
- from collections.abc import Mapping
5
- import pprint
6
-
7
- from _pytest._io.saferepr import saferepr
8
- from _pytest.assertion._typing import _HighlightFunc
9
-
10
-
11
- def _compare_eq_mapping(
12
- left: Mapping[object, object],
13
- right: Mapping[object, object],
14
- highlighter: _HighlightFunc,
15
- verbose: int = 0,
16
- ) -> Iterator[str]:
17
- set_left = set(left)
18
- set_right = set(right)
19
- common = set_left.intersection(set_right)
20
- same = {k: left[k] for k in common if left[k] == right[k]}
21
- if same and verbose < 2:
22
- yield f"Omitting {len(same)} identical items, use -vv to show"
23
- elif same:
24
- yield "Common items:"
25
- yield from highlighter(pprint.pformat(same)).splitlines()
26
- diff = {k for k in common if left[k] != right[k]}
27
- if diff:
28
- yield "Differing items:"
29
- for k in diff:
30
- yield (
31
- highlighter(saferepr({k: left[k]}))
32
- + " != "
33
- + highlighter(saferepr({k: right[k]}))
34
- )
35
- extra_left = set_left - set_right
36
- len_extra_left = len(extra_left)
37
- if len_extra_left:
38
- yield f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:"
39
- yield from highlighter(
40
- pprint.pformat({k: left[k] for k in extra_left})
41
- ).splitlines()
42
- extra_right = set_right - set_left
43
- len_extra_right = len(extra_right)
44
- if len_extra_right:
45
- yield f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:"
46
- yield from highlighter(
47
- pprint.pformat({k: right[k] for k in extra_right})
48
- ).splitlines()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/_compare_sequence.py DELETED
@@ -1,90 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Iterable
4
- from collections.abc import Iterator
5
- from collections.abc import Sequence
6
-
7
- from _pytest._io.pprint import PrettyPrinter
8
- from _pytest._io.saferepr import saferepr
9
- from _pytest.assertion._typing import _HighlightFunc
10
- from _pytest.compat import running_on_ci
11
-
12
-
13
- def _compare_eq_iterable(
14
- left: Iterable[object],
15
- right: Iterable[object],
16
- highlighter: _HighlightFunc,
17
- verbose: int = 0,
18
- ) -> Iterator[str]:
19
- if verbose <= 0 and not running_on_ci():
20
- yield "Use -v to get more diff"
21
- return
22
- # dynamic import to speedup pytest
23
- import difflib
24
-
25
- left_formatting = PrettyPrinter().pformat(left).splitlines()
26
- right_formatting = PrettyPrinter().pformat(right).splitlines()
27
-
28
- yield ""
29
- yield "Full diff:"
30
- # "right" is the expected base against which we compare "left",
31
- # see https://github.com/pytest-dev/pytest/issues/3333
32
- yield from highlighter(
33
- "\n".join(
34
- line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting)
35
- ),
36
- lexer="diff",
37
- ).splitlines()
38
-
39
-
40
- def _compare_eq_sequence(
41
- left: Sequence[object],
42
- right: Sequence[object],
43
- highlighter: _HighlightFunc,
44
- verbose: int = 0,
45
- ) -> Iterator[str]:
46
- comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
47
- len_left = len(left)
48
- len_right = len(right)
49
- for i in range(min(len_left, len_right)):
50
- if left[i] != right[i]:
51
- if comparing_bytes:
52
- # when comparing bytes, we want to see their ascii representation
53
- # instead of their numeric values (#5260)
54
- # using a slice gives us the ascii representation:
55
- # >>> s = b'foo'
56
- # >>> s[0]
57
- # 102
58
- # >>> s[0:1]
59
- # b'f'
60
- left_value: object = left[i : i + 1]
61
- right_value: object = right[i : i + 1]
62
- else:
63
- left_value = left[i]
64
- right_value = right[i]
65
-
66
- yield (
67
- f"At index {i} diff:"
68
- f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}"
69
- )
70
- break
71
-
72
- if comparing_bytes:
73
- # when comparing bytes, it doesn't help to show the "sides contain one or more
74
- # items" longer explanation, so skip it
75
- return
76
-
77
- len_diff = len_left - len_right
78
- if len_diff:
79
- if len_diff > 0:
80
- dir_with_more = "Left"
81
- extra = saferepr(left[len_right])
82
- else:
83
- len_diff = 0 - len_diff
84
- dir_with_more = "Right"
85
- extra = saferepr(right[len_left])
86
-
87
- if len_diff == 1:
88
- yield f"{dir_with_more} contains one more item: {highlighter(extra)}"
89
- else:
90
- yield f"{dir_with_more} contains {len_diff} more items, first extra item: {highlighter(extra)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/_compare_set.py DELETED
@@ -1,101 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Callable
4
- from collections.abc import Iterator
5
- from collections.abc import Set as AbstractSet
6
- from typing import TypeAlias
7
-
8
- from _pytest._io.saferepr import saferepr
9
- from _pytest.assertion._typing import _HighlightFunc
10
-
11
-
12
- def _set_one_sided_diff(
13
- posn: str,
14
- set1: AbstractSet[object],
15
- set2: AbstractSet[object],
16
- highlighter: _HighlightFunc,
17
- ) -> Iterator[str]:
18
- diff = set1 - set2
19
- if diff:
20
- yield f"Extra items in the {posn} set:"
21
- for item in diff:
22
- yield highlighter(saferepr(item))
23
-
24
-
25
- def _compare_eq_set(
26
- left: AbstractSet[object],
27
- right: AbstractSet[object],
28
- highlighter: _HighlightFunc,
29
- verbose: int = 0,
30
- ) -> Iterator[str]:
31
- yield from _set_one_sided_diff("left", left, right, highlighter)
32
- yield from _set_one_sided_diff("right", right, left, highlighter)
33
-
34
-
35
- def _compare_gte_set(
36
- left: AbstractSet[object],
37
- right: AbstractSet[object],
38
- highlighter: _HighlightFunc,
39
- verbose: int = 0,
40
- ) -> Iterator[str]:
41
- yield from _set_one_sided_diff("right", right, left, highlighter)
42
-
43
-
44
- def _compare_lte_set(
45
- left: AbstractSet[object],
46
- right: AbstractSet[object],
47
- highlighter: _HighlightFunc,
48
- verbose: int = 0,
49
- ) -> Iterator[str]:
50
- yield from _set_one_sided_diff("left", left, right, highlighter)
51
-
52
-
53
- def _compare_gt_set(
54
- left: AbstractSet[object],
55
- right: AbstractSet[object],
56
- highlighter: _HighlightFunc,
57
- verbose: int = 0,
58
- ) -> Iterator[str]:
59
- if left == right:
60
- yield "Both sets are equal"
61
- else:
62
- yield from _set_one_sided_diff("right", right, left, highlighter)
63
-
64
-
65
- def _compare_lt_set(
66
- left: AbstractSet[object],
67
- right: AbstractSet[object],
68
- highlighter: _HighlightFunc,
69
- verbose: int = 0,
70
- ) -> Iterator[str]:
71
- if left == right:
72
- yield "Both sets are equal"
73
- else:
74
- yield from _set_one_sided_diff("left", left, right, highlighter)
75
-
76
-
77
- SetComparisonFunction: TypeAlias = Callable[
78
- [AbstractSet[object], AbstractSet[object], _HighlightFunc, int],
79
- Iterator[str],
80
- ]
81
-
82
-
83
- def _both_sets_are_equal(
84
- left: AbstractSet[object],
85
- right: AbstractSet[object],
86
- highlighter: _HighlightFunc,
87
- verbose: int = 0,
88
- ) -> Iterator[str]:
89
- yield "Both sets are equal"
90
-
91
-
92
- SET_COMPARISON_FUNCTIONS: dict[str, SetComparisonFunction] = {
93
- # == can't be done here without a prior refactor because there's an additional
94
- # explanation for iterable in _compare_eq_any
95
- # "==": _compare_eq_set,
96
- "!=": _both_sets_are_equal,
97
- ">=": _compare_gte_set,
98
- "<=": _compare_lte_set,
99
- ">": _compare_gt_set,
100
- "<": _compare_lt_set,
101
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/_guards.py DELETED
@@ -1,60 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import collections.abc
4
- from collections.abc import Mapping
5
- import dataclasses
6
- from typing import TypeGuard
7
-
8
-
9
- def issequence(x: object) -> TypeGuard[collections.abc.Sequence[object]]:
10
- return isinstance(x, collections.abc.Sequence) and not isinstance(x, str)
11
-
12
-
13
- def istext(x: object) -> TypeGuard[str]:
14
- return isinstance(x, str)
15
-
16
-
17
- def ismapping(x: object) -> TypeGuard[Mapping[object, object]]:
18
- return isinstance(x, Mapping)
19
-
20
-
21
- def isset(x: object) -> TypeGuard[set[object] | frozenset[object]]:
22
- return isinstance(x, set | frozenset)
23
-
24
-
25
- def isnamedtuple(obj: object) -> bool:
26
- return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
27
-
28
-
29
- isdatacls = dataclasses.is_dataclass
30
-
31
-
32
- def isattrs(obj: object) -> bool:
33
- return getattr(obj, "__attrs_attrs__", None) is not None
34
-
35
-
36
- def isiterable(obj: object) -> TypeGuard[collections.abc.Iterable[object]]:
37
- try:
38
- iter(obj) # type: ignore[call-overload]
39
- return not istext(obj)
40
- except Exception:
41
- return False
42
-
43
-
44
- def has_default_eq(obj: object) -> bool:
45
- """Check if an instance of an object contains the default eq
46
-
47
- First, we check if the object's __eq__ attribute has __code__,
48
- if so, we check the equally of the method code filename (__code__.co_filename)
49
- to the default one generated by the dataclass and attr module
50
- for dataclasses the default co_filename is <string>, for attrs class, the __eq__ should contain "attrs eq generated"
51
- """
52
- # inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
53
- if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
54
- code_filename = obj.__eq__.__code__.co_filename
55
-
56
- if isattrs(obj):
57
- return "attrs generated " in code_filename
58
-
59
- return code_filename == "<string>" # data class
60
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/_typing.py DELETED
@@ -1,12 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Literal
4
- from typing import Protocol
5
-
6
-
7
- _AssertionTextDiffStyle = Literal["ndiff", "block"]
8
-
9
-
10
- class _HighlightFunc(Protocol): # noqa: PYI046
11
- def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str:
12
- """Apply highlighting to the given source."""
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/compare_text.py DELETED
@@ -1,104 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Iterator
4
-
5
- from _pytest._io.saferepr import saferepr
6
- from _pytest.assertion._typing import _AssertionTextDiffStyle
7
- from _pytest.assertion._typing import _HighlightFunc
8
- from _pytest.assertion.highlight import dummy_highlighter
9
- from _pytest.compat import assert_never
10
-
11
-
12
- def _compare_eq_text(
13
- left: str,
14
- right: str,
15
- highlighter: _HighlightFunc,
16
- verbose: int,
17
- assertion_text_diff_style: _AssertionTextDiffStyle,
18
- ) -> Iterator[str]:
19
- match assertion_text_diff_style:
20
- case "block":
21
- yield from _diff_text_block(left, right)
22
- case "ndiff":
23
- yield from _diff_text(left, right, highlighter, verbose)
24
- case unreachable:
25
- assert_never(unreachable)
26
-
27
-
28
- def _diff_text_block(left: str, right: str) -> Iterator[str]:
29
- yield "Left:"
30
- yield from _format_text_block_lines(left)
31
- yield ""
32
- yield "Right:"
33
- yield from _format_text_block_lines(right)
34
-
35
-
36
- def _format_text_block_lines(text: str) -> Iterator[str]:
37
- for line in text.split("\n"):
38
- yield f" {line}"
39
-
40
-
41
- def _diff_text(
42
- left: str, right: str, highlighter: _HighlightFunc, verbose: int = 0
43
- ) -> Iterator[str]:
44
- """Yield the explanation for the diff between text.
45
-
46
- Unless --verbose is used this will skip leading and trailing
47
- characters which are identical to keep the diff minimal.
48
- """
49
- from difflib import ndiff
50
-
51
- if verbose < 1:
52
- i = 0 # just in case left or right has zero length
53
- for i in range(min(len(left), len(right))):
54
- if left[i] != right[i]:
55
- break
56
- if i > 42:
57
- i -= 10 # Provide some context
58
- yield f"Skipping {i} identical leading characters in diff, use -v to show"
59
- left = left[i:]
60
- right = right[i:]
61
- if len(left) == len(right):
62
- for i in range(len(left)):
63
- if left[-i] != right[-i]:
64
- break
65
- if i > 42:
66
- i -= 10 # Provide some context
67
- yield (
68
- f"Skipping {i} identical trailing "
69
- "characters in diff, use -v to show"
70
- )
71
- left = left[:-i]
72
- right = right[:-i]
73
- keepends = True
74
- if left.isspace() or right.isspace():
75
- left = repr(str(left))
76
- right = repr(str(right))
77
- yield "Strings contain only whitespace, escaping them using repr()"
78
- # "right" is the expected base against which we compare "left",
79
- # see https://github.com/pytest-dev/pytest/issues/3333
80
- yield from highlighter(
81
- "\n".join(
82
- line.strip("\n")
83
- for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
84
- ),
85
- lexer="diff",
86
- ).splitlines()
87
-
88
-
89
- def _notin_text(term: str, text: str, verbose: int = 0) -> Iterator[str]:
90
- index = text.find(term)
91
- head = text[:index]
92
- tail = text[index + len(term) :]
93
- correct_text = head + tail
94
- diff = _diff_text(text, correct_text, dummy_highlighter, verbose)
95
- yield f"{saferepr(term, maxsize=42)} is contained here:"
96
- for line in diff:
97
- if line.startswith("Skipping"):
98
- continue
99
- if line.startswith("- "):
100
- continue
101
- if line.startswith("+ "):
102
- yield " " + line[2:]
103
- else:
104
- yield line
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/highlight.py DELETED
@@ -1,11 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Literal
4
-
5
-
6
- def dummy_highlighter(source: str, lexer: Literal["diff", "python"] = "python") -> str:
7
- """Dummy highlighter that returns the text unprocessed.
8
-
9
- Needed for _notin_text, as the diff gets post-processed to only show the "+" part.
10
- """
11
- return source
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/rewrite.py DELETED
@@ -1,1193 +0,0 @@
1
- """Rewrite assertion AST to produce nice error messages."""
2
-
3
- from __future__ import annotations
4
-
5
- import ast
6
- from collections import defaultdict
7
- from collections.abc import Callable
8
- from collections.abc import Iterable
9
- from collections.abc import Iterator
10
- from collections.abc import Sequence
11
- import errno
12
- import functools
13
- import importlib.abc
14
- import importlib.machinery
15
- import importlib.util
16
- import io
17
- import itertools
18
- import marshal
19
- import os
20
- from pathlib import Path
21
- from pathlib import PurePath
22
- import struct
23
- import sys
24
- import tokenize
25
- import types
26
- from typing import IO
27
- from typing import TYPE_CHECKING
28
-
29
-
30
- if sys.version_info >= (3, 12):
31
- from importlib.resources.abc import TraversableResources
32
- else:
33
- from importlib.abc import TraversableResources
34
- if sys.version_info < (3, 11):
35
- from importlib.readers import FileReader
36
- else:
37
- from importlib.resources.readers import FileReader
38
-
39
-
40
- from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
41
- from _pytest._io.saferepr import saferepr
42
- from _pytest._io.saferepr import saferepr_unlimited
43
- from _pytest._version import version
44
- from _pytest.assertion import util
45
- from _pytest.config import Config
46
- from _pytest.fixtures import FixtureFunctionDefinition
47
- from _pytest.main import Session
48
- from _pytest.pathlib import absolutepath
49
- from _pytest.pathlib import fnmatch_ex
50
- from _pytest.stash import StashKey
51
-
52
-
53
- # fmt: off
54
- from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip
55
- # fmt:on
56
-
57
- if TYPE_CHECKING:
58
- from _pytest.assertion import AssertionState
59
-
60
-
61
- class Sentinel:
62
- pass
63
-
64
-
65
- assertstate_key = StashKey["AssertionState"]()
66
-
67
- # pytest caches rewritten pycs in pycache dirs
68
- PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
69
- PYC_EXT = ".py" + ((__debug__ and "c") or "o")
70
- PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
71
-
72
- # Special marker that denotes we have just left a scope definition
73
- _SCOPE_END_MARKER = Sentinel()
74
-
75
-
76
- class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
77
- """PEP302/PEP451 import hook which rewrites asserts."""
78
-
79
- def __init__(self, config: Config) -> None:
80
- self.config = config
81
- try:
82
- self.fnpats = config.getini("python_files")
83
- except ValueError:
84
- self.fnpats = ["test_*.py", "*_test.py"]
85
- self.session: Session | None = None
86
- self._rewritten_names: dict[str, Path] = {}
87
- self._must_rewrite: set[str] = set()
88
- # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
89
- # which might result in infinite recursion (#3506)
90
- self._writing_pyc = False
91
- self._basenames_to_check_rewrite = {"conftest"}
92
- self._marked_for_rewrite_cache: dict[str, bool] = {}
93
- self._session_paths_checked = False
94
-
95
- def set_session(self, session: Session | None) -> None:
96
- self.session = session
97
- self._session_paths_checked = False
98
-
99
- # Indirection so we can mock calls to find_spec originated from the hook during testing
100
- _find_spec = importlib.machinery.PathFinder.find_spec
101
-
102
- def find_spec(
103
- self,
104
- name: str,
105
- path: Sequence[str | bytes] | None = None,
106
- target: types.ModuleType | None = None,
107
- ) -> importlib.machinery.ModuleSpec | None:
108
- if self._writing_pyc:
109
- return None
110
- state = self.config.stash[assertstate_key]
111
- if self._early_rewrite_bailout(name, state):
112
- return None
113
- state.trace(f"find_module called for: {name}")
114
-
115
- # Type ignored because mypy is confused about the `self` binding here.
116
- spec = self._find_spec(name, path) # type: ignore
117
-
118
- if (
119
- # the import machinery could not find a file to import
120
- spec is None
121
- # this is a namespace package (without `__init__.py`)
122
- # there's nothing to rewrite there
123
- or spec.origin is None
124
- # we can only rewrite source files
125
- or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
126
- # if the file doesn't exist, we can't rewrite it
127
- or not os.path.exists(spec.origin)
128
- ):
129
- return None
130
- else:
131
- fn = spec.origin
132
-
133
- if not self._should_rewrite(name, fn, state):
134
- return None
135
-
136
- return importlib.util.spec_from_file_location(
137
- name,
138
- fn,
139
- loader=self,
140
- submodule_search_locations=spec.submodule_search_locations,
141
- )
142
-
143
- def create_module(
144
- self, spec: importlib.machinery.ModuleSpec
145
- ) -> types.ModuleType | None:
146
- return None # default behaviour is fine
147
-
148
- def exec_module(self, module: types.ModuleType) -> None:
149
- assert module.__spec__ is not None
150
- assert module.__spec__.origin is not None
151
- fn = Path(module.__spec__.origin)
152
- state = self.config.stash[assertstate_key]
153
-
154
- self._rewritten_names[module.__name__] = fn
155
-
156
- # The requested module looks like a test file, so rewrite it. This is
157
- # the most magical part of the process: load the source, rewrite the
158
- # asserts, and load the rewritten source. We also cache the rewritten
159
- # module code in a special pyc. We must be aware of the possibility of
160
- # concurrent pytest processes rewriting and loading pycs. To avoid
161
- # tricky race conditions, we maintain the following invariant: The
162
- # cached pyc is always a complete, valid pyc. Operations on it must be
163
- # atomic. POSIX's atomic rename comes in handy.
164
- write = not sys.dont_write_bytecode
165
- cache_dir = get_cache_dir(fn)
166
- if write:
167
- ok = try_makedirs(cache_dir)
168
- if not ok:
169
- write = False
170
- state.trace(f"read only directory: {cache_dir}")
171
-
172
- cache_name = fn.name[:-3] + PYC_TAIL
173
- pyc = cache_dir / cache_name
174
- # Notice that even if we're in a read-only directory, I'm going
175
- # to check for a cached pyc. This may not be optimal...
176
- co = _read_pyc(fn, pyc, state.trace)
177
- if co is None:
178
- state.trace(f"rewriting {fn!r}")
179
- source_stat, co = _rewrite_test(fn, self.config)
180
- if write:
181
- self._writing_pyc = True
182
- try:
183
- _write_pyc(state, co, source_stat, pyc)
184
- finally:
185
- self._writing_pyc = False
186
- else:
187
- state.trace(f"found cached rewritten pyc for {fn}")
188
- exec(co, module.__dict__)
189
-
190
- def _early_rewrite_bailout(self, name: str, state: AssertionState) -> bool:
191
- """A fast way to get out of rewriting modules.
192
-
193
- Profiling has shown that the call to PathFinder.find_spec (inside of
194
- the find_spec from this class) is a major slowdown, so, this method
195
- tries to filter what we're sure won't be rewritten before getting to
196
- it.
197
- """
198
- if self.session is not None and not self._session_paths_checked:
199
- self._session_paths_checked = True
200
- for initial_path in self.session._initialpaths:
201
- # Make something as c:/projects/my_project/path.py ->
202
- # ['c:', 'projects', 'my_project', 'path.py']
203
- parts = str(initial_path).split(os.sep)
204
- # add 'path' to basenames to be checked.
205
- self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
206
-
207
- # Note: conftest already by default in _basenames_to_check_rewrite.
208
- parts = name.split(".")
209
- if parts[-1] in self._basenames_to_check_rewrite:
210
- return False
211
-
212
- # For matching the name it must be as if it was a filename.
213
- path = PurePath(*parts).with_suffix(".py")
214
-
215
- for pat in self.fnpats:
216
- # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
217
- # on the name alone because we need to match against the full path
218
- if os.path.dirname(pat):
219
- return False
220
- if fnmatch_ex(pat, path):
221
- return False
222
-
223
- if self._is_marked_for_rewrite(name, state):
224
- return False
225
-
226
- state.trace(f"early skip of rewriting module: {name}")
227
- return True
228
-
229
- def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool:
230
- # always rewrite conftest files
231
- if os.path.basename(fn) == "conftest.py":
232
- state.trace(f"rewriting conftest file: {fn!r}")
233
- return True
234
-
235
- if self.session is not None:
236
- if self.session.isinitpath(absolutepath(fn)):
237
- state.trace(f"matched test file (was specified on cmdline): {fn!r}")
238
- return True
239
-
240
- # modules not passed explicitly on the command line are only
241
- # rewritten if they match the naming convention for test files
242
- fn_path = PurePath(fn)
243
- for pat in self.fnpats:
244
- if fnmatch_ex(pat, fn_path):
245
- state.trace(f"matched test file {fn!r}")
246
- return True
247
-
248
- return self._is_marked_for_rewrite(name, state)
249
-
250
- def _is_marked_for_rewrite(self, name: str, state: AssertionState) -> bool:
251
- try:
252
- return self._marked_for_rewrite_cache[name]
253
- except KeyError:
254
- for marked in self._must_rewrite:
255
- if name == marked or name.startswith(marked + "."):
256
- state.trace(f"matched marked file {name!r} (from {marked!r})")
257
- self._marked_for_rewrite_cache[name] = True
258
- return True
259
-
260
- self._marked_for_rewrite_cache[name] = False
261
- return False
262
-
263
- def mark_rewrite(self, *names: str) -> None:
264
- """Mark import names as needing to be rewritten.
265
-
266
- The named module or package as well as any nested modules will
267
- be rewritten on import.
268
- """
269
- already_imported = (
270
- set(names).intersection(sys.modules).difference(self._rewritten_names)
271
- )
272
- for name in already_imported:
273
- mod = sys.modules[name]
274
- if not AssertionRewriter.is_rewrite_disabled(
275
- mod.__doc__ or ""
276
- ) and not isinstance(mod.__loader__, type(self)):
277
- self._warn_already_imported(name)
278
- self._must_rewrite.update(names)
279
- self._marked_for_rewrite_cache.clear()
280
-
281
- def _warn_already_imported(self, name: str) -> None:
282
- from _pytest.warning_types import PytestAssertRewriteWarning
283
-
284
- self.config.issue_config_time_warning(
285
- PytestAssertRewriteWarning(
286
- f"Module already imported so cannot be rewritten; {name}"
287
- ),
288
- stacklevel=5,
289
- )
290
-
291
- def get_data(self, pathname: str | bytes) -> bytes:
292
- """Optional PEP302 get_data API."""
293
- with open(pathname, "rb") as f:
294
- return f.read()
295
-
296
- def get_resource_reader(self, name: str) -> TraversableResources:
297
- return FileReader(types.SimpleNamespace(path=self._rewritten_names[name])) # type: ignore[arg-type]
298
-
299
-
300
- def _write_pyc_fp(
301
- fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType
302
- ) -> None:
303
- # Technically, we don't have to have the same pyc format as
304
- # (C)Python, since these "pycs" should never be seen by builtin
305
- # import. However, there's little reason to deviate.
306
- fp.write(importlib.util.MAGIC_NUMBER)
307
- # https://www.python.org/dev/peps/pep-0552/
308
- flags = b"\x00\x00\x00\x00"
309
- fp.write(flags)
310
- # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
311
- mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
312
- size = source_stat.st_size & 0xFFFFFFFF
313
- # "<LL" stands for 2 unsigned longs, little-endian.
314
- fp.write(struct.pack("<LL", mtime, size))
315
- fp.write(marshal.dumps(co))
316
-
317
-
318
- def _write_pyc(
319
- state: AssertionState,
320
- co: types.CodeType,
321
- source_stat: os.stat_result,
322
- pyc: Path,
323
- ) -> bool:
324
- proc_pyc = f"{pyc}.{os.getpid()}"
325
- try:
326
- with open(proc_pyc, "wb") as fp:
327
- _write_pyc_fp(fp, source_stat, co)
328
- except OSError as e:
329
- state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
330
- return False
331
-
332
- try:
333
- os.replace(proc_pyc, pyc)
334
- except OSError as e:
335
- state.trace(f"error writing pyc file at {pyc}: {e}")
336
- # we ignore any failure to write the cache file
337
- # there are many reasons, permission-denied, pycache dir being a
338
- # file etc.
339
- return False
340
- return True
341
-
342
-
343
- def _rewrite_test(fn: Path, config: Config) -> tuple[os.stat_result, types.CodeType]:
344
- """Read and rewrite *fn* and return the code object."""
345
- stat = os.stat(fn)
346
- source = fn.read_bytes()
347
- strfn = str(fn)
348
- tree = ast.parse(source, filename=strfn)
349
- rewrite_asserts(tree, source, strfn, config)
350
- co = compile(tree, strfn, "exec", dont_inherit=True)
351
- return stat, co
352
-
353
-
354
- def _read_pyc(
355
- source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None
356
- ) -> types.CodeType | None:
357
- """Possibly read a pytest pyc containing rewritten code.
358
-
359
- Return rewritten code if successful or None if not.
360
- """
361
- try:
362
- fp = open(pyc, "rb")
363
- except OSError:
364
- return None
365
- with fp:
366
- try:
367
- stat_result = os.stat(source)
368
- mtime = int(stat_result.st_mtime)
369
- size = stat_result.st_size
370
- data = fp.read(16)
371
- except OSError as e:
372
- trace(f"_read_pyc({source}): OSError {e}")
373
- return None
374
- # Check for invalid or out of date pyc file.
375
- if len(data) != (16):
376
- trace(f"_read_pyc({source}): invalid pyc (too short)")
377
- return None
378
- if data[:4] != importlib.util.MAGIC_NUMBER:
379
- trace(f"_read_pyc({source}): invalid pyc (bad magic number)")
380
- return None
381
- if data[4:8] != b"\x00\x00\x00\x00":
382
- trace(f"_read_pyc({source}): invalid pyc (unsupported flags)")
383
- return None
384
- mtime_data = data[8:12]
385
- if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
386
- trace(f"_read_pyc({source}): out of date")
387
- return None
388
- size_data = data[12:16]
389
- if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
390
- trace(f"_read_pyc({source}): invalid pyc (incorrect size)")
391
- return None
392
- try:
393
- co = marshal.load(fp)
394
- except Exception as e:
395
- trace(f"_read_pyc({source}): marshal.load error {e}")
396
- return None
397
- if not isinstance(co, types.CodeType):
398
- trace(f"_read_pyc({source}): not a code object")
399
- return None
400
- return co
401
-
402
-
403
- def rewrite_asserts(
404
- mod: ast.Module,
405
- source: bytes,
406
- module_path: str | None = None,
407
- config: Config | None = None,
408
- ) -> None:
409
- """Rewrite the assert statements in mod."""
410
- AssertionRewriter(module_path, config, source).run(mod)
411
-
412
-
413
- def _saferepr(obj: object) -> str:
414
- r"""Get a safe repr of an object for assertion error messages.
415
-
416
- The assertion formatting (util.format_explanation()) requires
417
- newlines to be escaped since they are a special character for it.
418
- Normally assertion.util.format_explanation() does this but for a
419
- custom repr it is possible to contain one of the special escape
420
- sequences, especially '\n{' and '\n}' are likely to be present in
421
- JSON reprs.
422
- """
423
- if isinstance(obj, types.MethodType):
424
- # for bound methods, skip redundant <bound method ...> information
425
- return obj.__name__
426
-
427
- maxsize = _get_maxsize_for_saferepr(util._config)
428
- if not maxsize:
429
- return saferepr_unlimited(obj).replace("\n", "\\n")
430
- return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
431
-
432
-
433
- def _get_maxsize_for_saferepr(config: Config | None) -> int | None:
434
- """Get `maxsize` configuration for saferepr based on the given config object."""
435
- if config is None:
436
- verbosity = 0
437
- else:
438
- verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
439
- if verbosity >= 2:
440
- return None
441
- if verbosity >= 1:
442
- return DEFAULT_REPR_MAX_SIZE * 10
443
- return DEFAULT_REPR_MAX_SIZE
444
-
445
-
446
- def _format_assertmsg(obj: object) -> str:
447
- r"""Format the custom assertion message given.
448
-
449
- For strings this simply replaces newlines with '\n~' so that
450
- util.format_explanation() will preserve them instead of escaping
451
- newlines. For other objects saferepr() is used first.
452
- """
453
- # reprlib appears to have a bug which means that if a string
454
- # contains a newline it gets escaped, however if an object has a
455
- # .__repr__() which contains newlines it does not get escaped.
456
- # However in either case we want to preserve the newline.
457
- replaces = [("\n", "\n~"), ("%", "%%")]
458
- if not isinstance(obj, str):
459
- obj = saferepr(obj, _get_maxsize_for_saferepr(util._config))
460
- replaces.append(("\\n", "\n~"))
461
-
462
- for r1, r2 in replaces:
463
- obj = obj.replace(r1, r2)
464
-
465
- return obj
466
-
467
-
468
- def _should_repr_global_name(obj: object) -> bool:
469
- if callable(obj):
470
- # For pytest fixtures the __repr__ method provides more information than the function name.
471
- return isinstance(obj, FixtureFunctionDefinition)
472
-
473
- try:
474
- return not hasattr(obj, "__name__")
475
- except Exception:
476
- return True
477
-
478
-
479
- def _format_boolop(explanations: Iterable[str], is_or: bool) -> str:
480
- explanation = "(" + ((is_or and " or ") or " and ").join(explanations) + ")"
481
- return explanation.replace("%", "%%")
482
-
483
-
484
- def _call_reprcompare(
485
- ops: Sequence[str],
486
- results: Sequence[bool],
487
- expls: Sequence[str],
488
- each_obj: Sequence[object],
489
- ) -> str:
490
- for i, res, expl in zip(range(len(ops)), results, expls, strict=True):
491
- try:
492
- done = not res
493
- except Exception:
494
- done = True
495
- if done:
496
- break
497
- if util._reprcompare is not None:
498
- custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1])
499
- if custom is not None:
500
- return custom
501
- return expl
502
-
503
-
504
- def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None:
505
- if util._assertion_pass is not None:
506
- util._assertion_pass(lineno, orig, expl)
507
-
508
-
509
- def _check_if_assertion_pass_impl() -> bool:
510
- """Check if any plugins implement the pytest_assertion_pass hook
511
- in order not to generate explanation unnecessarily (might be expensive)."""
512
- return True if util._assertion_pass else False
513
-
514
-
515
- UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}
516
-
517
- BINOP_MAP = {
518
- ast.BitOr: "|",
519
- ast.BitXor: "^",
520
- ast.BitAnd: "&",
521
- ast.LShift: "<<",
522
- ast.RShift: ">>",
523
- ast.Add: "+",
524
- ast.Sub: "-",
525
- ast.Mult: "*",
526
- ast.Div: "/",
527
- ast.FloorDiv: "//",
528
- ast.Mod: "%%", # escaped for string formatting
529
- ast.Eq: "==",
530
- ast.NotEq: "!=",
531
- ast.Lt: "<",
532
- ast.LtE: "<=",
533
- ast.Gt: ">",
534
- ast.GtE: ">=",
535
- ast.Pow: "**",
536
- ast.Is: "is",
537
- ast.IsNot: "is not",
538
- ast.In: "in",
539
- ast.NotIn: "not in",
540
- ast.MatMult: "@",
541
- }
542
-
543
-
544
- def traverse_node(node: ast.AST) -> Iterator[ast.AST]:
545
- """Recursively yield node and all its children in depth-first order."""
546
- yield node
547
- for child in ast.iter_child_nodes(node):
548
- yield from traverse_node(child)
549
-
550
-
551
- @functools.lru_cache(maxsize=1)
552
- def _get_assertion_exprs(src: bytes) -> dict[int, str]:
553
- """Return a mapping from {lineno: "assertion test expression"}."""
554
- ret: dict[int, str] = {}
555
-
556
- depth = 0
557
- lines: list[str] = []
558
- assert_lineno: int | None = None
559
- seen_lines: set[int] = set()
560
-
561
- def _write_and_reset() -> None:
562
- nonlocal depth, lines, assert_lineno, seen_lines
563
- assert assert_lineno is not None
564
- ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\")
565
- depth = 0
566
- lines = []
567
- assert_lineno = None
568
- seen_lines = set()
569
-
570
- tokens = tokenize.tokenize(io.BytesIO(src).readline)
571
- for tp, source, (lineno, offset), _, line in tokens:
572
- if tp == tokenize.NAME and source == "assert":
573
- assert_lineno = lineno
574
- elif assert_lineno is not None:
575
- # keep track of depth for the assert-message `,` lookup
576
- if tp == tokenize.OP and source in "([{":
577
- depth += 1
578
- elif tp == tokenize.OP and source in ")]}":
579
- depth -= 1
580
-
581
- if not lines:
582
- lines.append(line[offset:])
583
- seen_lines.add(lineno)
584
- # a non-nested comma separates the expression from the message
585
- elif depth == 0 and tp == tokenize.OP and source == ",":
586
- # one line assert with message
587
- if lineno in seen_lines and len(lines) == 1:
588
- offset_in_trimmed = offset + len(lines[-1]) - len(line)
589
- lines[-1] = lines[-1][:offset_in_trimmed]
590
- # multi-line assert with message
591
- elif lineno in seen_lines:
592
- lines[-1] = lines[-1][:offset]
593
- # multi line assert with escaped newline before message
594
- else:
595
- lines.append(line[:offset])
596
- _write_and_reset()
597
- elif tp in {tokenize.NEWLINE, tokenize.ENDMARKER}:
598
- _write_and_reset()
599
- elif lines and lineno not in seen_lines:
600
- lines.append(line)
601
- seen_lines.add(lineno)
602
-
603
- return ret
604
-
605
-
606
- class AssertionRewriter(ast.NodeVisitor):
607
- """Assertion rewriting implementation.
608
-
609
- The main entrypoint is to call .run() with an ast.Module instance,
610
- this will then find all the assert statements and rewrite them to
611
- provide intermediate values and a detailed assertion error. See
612
- http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
613
- for an overview of how this works.
614
-
615
- The entry point here is .run() which will iterate over all the
616
- statements in an ast.Module and for each ast.Assert statement it
617
- finds call .visit() with it. Then .visit_Assert() takes over and
618
- is responsible for creating new ast statements to replace the
619
- original assert statement: it rewrites the test of an assertion
620
- to provide intermediate values and replace it with an if statement
621
- which raises an assertion error with a detailed explanation in
622
- case the expression is false and calls pytest_assertion_pass hook
623
- if expression is true.
624
-
625
- For this .visit_Assert() uses the visitor pattern to visit all the
626
- AST nodes of the ast.Assert.test field, each visit call returning
627
- an AST node and the corresponding explanation string. During this
628
- state is kept in several instance attributes:
629
-
630
- :statements: All the AST statements which will replace the assert
631
- statement.
632
-
633
- :variables: This is populated by .variable() with each variable
634
- used by the statements so that they can all be set to None at
635
- the end of the statements.
636
-
637
- :variable_counter: Counter to create new unique variables needed
638
- by statements. Variables are created using .variable() and
639
- have the form of "@py_assert0".
640
-
641
- :expl_stmts: The AST statements which will be executed to get
642
- data from the assertion. This is the code which will construct
643
- the detailed assertion message that is used in the AssertionError
644
- or for the pytest_assertion_pass hook.
645
-
646
- :explanation_specifiers: A dict filled by .explanation_param()
647
- with %-formatting placeholders and their corresponding
648
- expressions to use in the building of an assertion message.
649
- This is used by .pop_format_context() to build a message.
650
-
651
- :stack: A stack of the explanation_specifiers dicts maintained by
652
- .push_format_context() and .pop_format_context() which allows
653
- to build another %-formatted string while already building one.
654
-
655
- :scope: A tuple containing the current scope used for variables_overwrite.
656
-
657
- :variables_overwrite: A dict filled with references to variables
658
- that change value within an assert. This happens when a variable is
659
- reassigned with the walrus operator
660
-
661
- This state, except the variables_overwrite, is reset on every new assert
662
- statement visited and used by the other visitors.
663
- """
664
-
665
- def __init__(
666
- self, module_path: str | None, config: Config | None, source: bytes
667
- ) -> None:
668
- super().__init__()
669
- self.module_path = module_path
670
- self.config = config
671
- if config is not None:
672
- self.enable_assertion_pass_hook = config.getini(
673
- "enable_assertion_pass_hook"
674
- )
675
- else:
676
- self.enable_assertion_pass_hook = False
677
- self.source = source
678
- self.scope: tuple[ast.AST, ...] = ()
679
- self.variables_overwrite: defaultdict[tuple[ast.AST, ...], dict[str, str]] = (
680
- defaultdict(dict)
681
- )
682
-
683
- def run(self, mod: ast.Module) -> None:
684
- """Find all assert statements in *mod* and rewrite them."""
685
- if not mod.body:
686
- # Nothing to do.
687
- return
688
-
689
- # We'll insert some special imports at the top of the module, but after any
690
- # docstrings and __future__ imports, so first figure out where that is.
691
- doc = getattr(mod, "docstring", None)
692
- expect_docstring = doc is None
693
- if doc is not None and self.is_rewrite_disabled(doc):
694
- return
695
- pos = 0
696
- for item in mod.body:
697
- match item:
698
- case ast.Expr(value=ast.Constant(value=str() as doc)) if (
699
- expect_docstring
700
- ):
701
- if self.is_rewrite_disabled(doc):
702
- return
703
- expect_docstring = False
704
- case ast.ImportFrom(level=0, module="__future__"):
705
- pass
706
- case _:
707
- break
708
- pos += 1
709
- # Special case: for a decorated function, set the lineno to that of the
710
- # first decorator, not the `def`. Issue #4984.
711
- if isinstance(item, ast.FunctionDef) and item.decorator_list:
712
- lineno = item.decorator_list[0].lineno
713
- else:
714
- lineno = item.lineno
715
- # Now actually insert the special imports.
716
- aliases = [
717
- ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0),
718
- ast.alias(
719
- "_pytest.assertion.rewrite",
720
- "@pytest_ar",
721
- lineno=lineno,
722
- col_offset=0,
723
- ),
724
- ]
725
- imports = [
726
- ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases
727
- ]
728
- mod.body[pos:pos] = imports
729
-
730
- # Collect asserts.
731
- self.scope = (mod,)
732
- nodes: list[ast.AST | Sentinel] = [mod]
733
- while nodes:
734
- node = nodes.pop()
735
- if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
736
- self.scope = tuple((*self.scope, node))
737
- nodes.append(_SCOPE_END_MARKER)
738
- if node == _SCOPE_END_MARKER:
739
- self.scope = self.scope[:-1]
740
- continue
741
- assert isinstance(node, ast.AST)
742
- for name, field in ast.iter_fields(node):
743
- if isinstance(field, list):
744
- new: list[ast.AST] = []
745
- for i, child in enumerate(field):
746
- if isinstance(child, ast.Assert):
747
- # Transform assert.
748
- new.extend(self.visit(child))
749
- else:
750
- new.append(child)
751
- if isinstance(child, ast.AST):
752
- nodes.append(child)
753
- setattr(node, name, new)
754
- elif (
755
- isinstance(field, ast.AST)
756
- # Don't recurse into expressions as they can't contain
757
- # asserts.
758
- and not isinstance(field, ast.expr)
759
- ):
760
- nodes.append(field)
761
-
762
- @staticmethod
763
- def is_rewrite_disabled(docstring: str) -> bool:
764
- return "PYTEST_DONT_REWRITE" in docstring
765
-
766
- def variable(self) -> str:
767
- """Get a new variable."""
768
- # Use a character invalid in python identifiers to avoid clashing.
769
- name = "@py_assert" + str(next(self.variable_counter))
770
- self.variables.append(name)
771
- return name
772
-
773
- def assign(self, expr: ast.expr) -> ast.Name:
774
- """Give *expr* a name."""
775
- name = self.variable()
776
- self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr))
777
- return ast.copy_location(ast.Name(name, ast.Load()), expr)
778
-
779
- def display(self, expr: ast.expr) -> ast.expr:
780
- """Call saferepr on the expression."""
781
- return self.helper("_saferepr", expr)
782
-
783
- def helper(self, name: str, *args: ast.expr) -> ast.expr:
784
- """Call a helper in this module."""
785
- py_name = ast.Name("@pytest_ar", ast.Load())
786
- attr = ast.Attribute(py_name, name, ast.Load())
787
- return ast.Call(attr, list(args), [])
788
-
789
- def builtin(self, name: str) -> ast.Attribute:
790
- """Return the builtin called *name*."""
791
- builtin_name = ast.Name("@py_builtins", ast.Load())
792
- return ast.Attribute(builtin_name, name, ast.Load())
793
-
794
- def explanation_param(self, expr: ast.expr) -> str:
795
- """Return a new named %-formatting placeholder for expr.
796
-
797
- This creates a %-formatting placeholder for expr in the
798
- current formatting context, e.g. ``%(py0)s``. The placeholder
799
- and expr are placed in the current format context so that it
800
- can be used on the next call to .pop_format_context().
801
- """
802
- specifier = "py" + str(next(self.variable_counter))
803
- self.explanation_specifiers[specifier] = expr
804
- return "%(" + specifier + ")s"
805
-
806
- def push_format_context(self) -> None:
807
- """Create a new formatting context.
808
-
809
- The format context is used for when an explanation wants to
810
- have a variable value formatted in the assertion message. In
811
- this case the value required can be added using
812
- .explanation_param(). Finally .pop_format_context() is used
813
- to format a string of %-formatted values as added by
814
- .explanation_param().
815
- """
816
- self.explanation_specifiers: dict[str, ast.expr] = {}
817
- self.stack.append(self.explanation_specifiers)
818
-
819
- def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
820
- """Format the %-formatted string with current format context.
821
-
822
- The expl_expr should be an str ast.expr instance constructed from
823
- the %-placeholders created by .explanation_param(). This will
824
- add the required code to format said string to .expl_stmts and
825
- return the ast.Name instance of the formatted string.
826
- """
827
- current = self.stack.pop()
828
- if self.stack:
829
- self.explanation_specifiers = self.stack[-1]
830
- keys: list[ast.expr | None] = [ast.Constant(key) for key in current.keys()]
831
- format_dict = ast.Dict(keys, list(current.values()))
832
- form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
833
- name = "@py_format" + str(next(self.variable_counter))
834
- if self.enable_assertion_pass_hook:
835
- self.format_variables.append(name)
836
- self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form))
837
- return ast.Name(name, ast.Load())
838
-
839
- def generic_visit(self, node: ast.AST) -> tuple[ast.Name, str]:
840
- """Handle expressions we don't have custom code for."""
841
- assert isinstance(node, ast.expr)
842
- res = self.assign(node)
843
- return res, self.explanation_param(self.display(res))
844
-
845
- def visit_Assert(self, assert_: ast.Assert) -> list[ast.stmt]:
846
- """Return the AST statements to replace the ast.Assert instance.
847
-
848
- This rewrites the test of an assertion to provide
849
- intermediate values and replace it with an if statement which
850
- raises an assertion error with a detailed explanation in case
851
- the expression is false.
852
- """
853
- if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
854
- import warnings
855
-
856
- from _pytest.warning_types import PytestAssertRewriteWarning
857
-
858
- # TODO: This assert should not be needed.
859
- assert self.module_path is not None
860
- warnings.warn_explicit(
861
- PytestAssertRewriteWarning(
862
- "assertion is always true, perhaps remove parentheses?"
863
- ),
864
- category=None,
865
- filename=self.module_path,
866
- lineno=assert_.lineno,
867
- )
868
-
869
- self.statements: list[ast.stmt] = []
870
- self.variables: list[str] = []
871
- self.variable_counter = itertools.count()
872
-
873
- if self.enable_assertion_pass_hook:
874
- self.format_variables: list[str] = []
875
-
876
- self.stack: list[dict[str, ast.expr]] = []
877
- self.expl_stmts: list[ast.stmt] = []
878
- self.push_format_context()
879
- # Rewrite assert into a bunch of statements.
880
- top_condition, explanation = self.visit(assert_.test)
881
-
882
- negation = ast.UnaryOp(ast.Not(), top_condition)
883
-
884
- if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
885
- msg = self.pop_format_context(ast.Constant(explanation))
886
-
887
- # Failed
888
- if assert_.msg:
889
- assertmsg = self.helper("_format_assertmsg", assert_.msg)
890
- gluestr = "\n>assert "
891
- else:
892
- assertmsg = ast.Constant("")
893
- gluestr = "assert "
894
- err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg)
895
- err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
896
- err_name = ast.Name("AssertionError", ast.Load())
897
- fmt = self.helper("_format_explanation", err_msg)
898
- exc = ast.Call(err_name, [fmt], [])
899
- raise_ = ast.Raise(exc, None)
900
- statements_fail = []
901
- statements_fail.extend(self.expl_stmts)
902
- statements_fail.append(raise_)
903
-
904
- # Passed
905
- fmt_pass = self.helper("_format_explanation", msg)
906
- orig = _get_assertion_exprs(self.source)[assert_.lineno]
907
- hook_call_pass = ast.Expr(
908
- self.helper(
909
- "_call_assertion_pass",
910
- ast.Constant(assert_.lineno),
911
- ast.Constant(orig),
912
- fmt_pass,
913
- )
914
- )
915
- # If any hooks implement assert_pass hook
916
- hook_impl_test = ast.If(
917
- self.helper("_check_if_assertion_pass_impl"),
918
- [*self.expl_stmts, hook_call_pass],
919
- [],
920
- )
921
- statements_pass: list[ast.stmt] = [hook_impl_test]
922
-
923
- # Test for assertion condition
924
- main_test = ast.If(negation, statements_fail, statements_pass)
925
- self.statements.append(main_test)
926
- if self.format_variables:
927
- variables: list[ast.expr] = [
928
- ast.Name(name, ast.Store()) for name in self.format_variables
929
- ]
930
- clear_format = ast.Assign(variables, ast.Constant(None))
931
- self.statements.append(clear_format)
932
-
933
- else: # Original assertion rewriting
934
- # Create failure message.
935
- body = self.expl_stmts
936
- self.statements.append(ast.If(negation, body, []))
937
- if assert_.msg:
938
- assertmsg = self.helper("_format_assertmsg", assert_.msg)
939
- explanation = "\n>assert " + explanation
940
- else:
941
- assertmsg = ast.Constant("")
942
- explanation = "assert " + explanation
943
- template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation))
944
- msg = self.pop_format_context(template)
945
- fmt = self.helper("_format_explanation", msg)
946
- err_name = ast.Name("AssertionError", ast.Load())
947
- exc = ast.Call(err_name, [fmt], [])
948
- raise_ = ast.Raise(exc, None)
949
-
950
- body.append(raise_)
951
-
952
- # Clear temporary variables by setting them to None.
953
- if self.variables:
954
- variables = [ast.Name(name, ast.Store()) for name in self.variables]
955
- clear = ast.Assign(variables, ast.Constant(None))
956
- self.statements.append(clear)
957
- # Fix locations (line numbers/column offsets).
958
- for stmt in self.statements:
959
- for node in traverse_node(stmt):
960
- if getattr(node, "lineno", None) is None:
961
- # apply the assertion location to all generated ast nodes without source location
962
- # and preserve the location of existing nodes or generated nodes with an correct location.
963
- ast.copy_location(node, assert_)
964
- return self.statements
965
-
966
- def visit_NamedExpr(self, name: ast.NamedExpr) -> tuple[ast.NamedExpr, str]:
967
- # This method handles the 'walrus operator' repr of the target
968
- # name if it's a local variable or _should_repr_global_name()
969
- # thinks it's acceptable.
970
- locs = ast.Call(self.builtin("locals"), [], [])
971
- target_id = name.target.id
972
- inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
973
- dorepr = self.helper("_should_repr_global_name", name)
974
- test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
975
- expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
976
- return name, self.explanation_param(expr)
977
-
978
- def visit_Name(self, name: ast.Name) -> tuple[ast.Name, str]:
979
- # Display the repr of the name if it's a local variable or
980
- # _should_repr_global_name() thinks it's acceptable.
981
- locs = ast.Call(self.builtin("locals"), [], [])
982
- inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs])
983
- dorepr = self.helper("_should_repr_global_name", name)
984
- test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
985
- expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
986
- return name, self.explanation_param(expr)
987
-
988
- def visit_BoolOp(self, boolop: ast.BoolOp) -> tuple[ast.Name, str]:
989
- res_var = self.variable()
990
- expl_list = self.assign(ast.List([], ast.Load()))
991
- app = ast.Attribute(expl_list, "append", ast.Load())
992
- is_or = int(isinstance(boolop.op, ast.Or))
993
- body = save = self.statements
994
- fail_save = self.expl_stmts
995
- levels = len(boolop.values) - 1
996
- self.push_format_context()
997
- # Process each operand, short-circuiting if needed.
998
- for i, v in enumerate(boolop.values):
999
- if i:
1000
- fail_inner: list[ast.stmt] = []
1001
- # cond is set in a prior loop iteration below
1002
- self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
1003
- self.expl_stmts = fail_inner
1004
- match v:
1005
- # Check if the left operand is an ast.NamedExpr and the value has already been visited
1006
- case ast.Compare(
1007
- left=ast.NamedExpr(target=ast.Name(id=target_id))
1008
- ) if target_id in [
1009
- e.id for e in boolop.values[:i] if hasattr(e, "id")
1010
- ]:
1011
- pytest_temp = self.variable()
1012
- self.variables_overwrite[self.scope][target_id] = v.left # type:ignore[assignment]
1013
- # mypy's false positive, we're checking that the 'target' attribute exists.
1014
- v.left.target.id = pytest_temp # type:ignore[attr-defined]
1015
- self.push_format_context()
1016
- res, expl = self.visit(v)
1017
- body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
1018
- expl_format = self.pop_format_context(ast.Constant(expl))
1019
- call = ast.Call(app, [expl_format], [])
1020
- self.expl_stmts.append(ast.Expr(call))
1021
- if i < levels:
1022
- cond: ast.expr = res
1023
- if is_or:
1024
- cond = ast.UnaryOp(ast.Not(), cond)
1025
- inner: list[ast.stmt] = []
1026
- self.statements.append(ast.If(cond, inner, []))
1027
- self.statements = body = inner
1028
- self.statements = save
1029
- self.expl_stmts = fail_save
1030
- expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or))
1031
- expl = self.pop_format_context(expl_template)
1032
- return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
1033
-
1034
- def visit_UnaryOp(self, unary: ast.UnaryOp) -> tuple[ast.Name, str]:
1035
- pattern = UNARY_MAP[unary.op.__class__]
1036
- operand_res, operand_expl = self.visit(unary.operand)
1037
- res = self.assign(ast.copy_location(ast.UnaryOp(unary.op, operand_res), unary))
1038
- return res, pattern % (operand_expl,)
1039
-
1040
- def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]:
1041
- symbol = BINOP_MAP[binop.op.__class__]
1042
- left_expr, left_expl = self.visit(binop.left)
1043
- right_expr, right_expl = self.visit(binop.right)
1044
- explanation = f"({left_expl} {symbol} {right_expl})"
1045
- res = self.assign(
1046
- ast.copy_location(ast.BinOp(left_expr, binop.op, right_expr), binop)
1047
- )
1048
- return res, explanation
1049
-
1050
- def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]:
1051
- new_func, func_expl = self.visit(call.func)
1052
- arg_expls = []
1053
- new_args = []
1054
- new_kwargs = []
1055
- for arg in call.args:
1056
- if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
1057
- self.scope, {}
1058
- ):
1059
- arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
1060
- res, expl = self.visit(arg)
1061
- arg_expls.append(expl)
1062
- new_args.append(res)
1063
- for keyword in call.keywords:
1064
- match keyword.value:
1065
- case ast.Name(id=id) if id in self.variables_overwrite.get(
1066
- self.scope, {}
1067
- ):
1068
- keyword.value = self.variables_overwrite[self.scope][id] # type:ignore[assignment]
1069
- res, expl = self.visit(keyword.value)
1070
- new_kwargs.append(ast.keyword(keyword.arg, res))
1071
- if keyword.arg:
1072
- arg_expls.append(keyword.arg + "=" + expl)
1073
- else: # **args have `arg` keywords with an .arg of None
1074
- arg_expls.append("**" + expl)
1075
-
1076
- expl = "{}({})".format(func_expl, ", ".join(arg_expls))
1077
- new_call = ast.copy_location(ast.Call(new_func, new_args, new_kwargs), call)
1078
- res = self.assign(new_call)
1079
- res_expl = self.explanation_param(self.display(res))
1080
- outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}"
1081
- return res, outer_expl
1082
-
1083
- def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]:
1084
- # A Starred node can appear in a function call.
1085
- res, expl = self.visit(starred.value)
1086
- new_starred = ast.Starred(res, starred.ctx)
1087
- return new_starred, "*" + expl
1088
-
1089
- def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]:
1090
- if not isinstance(attr.ctx, ast.Load):
1091
- return self.generic_visit(attr)
1092
- value, value_expl = self.visit(attr.value)
1093
- res = self.assign(
1094
- ast.copy_location(ast.Attribute(value, attr.attr, ast.Load()), attr)
1095
- )
1096
- res_expl = self.explanation_param(self.display(res))
1097
- pat = "%s\n{%s = %s.%s\n}"
1098
- expl = pat % (res_expl, res_expl, value_expl, attr.attr)
1099
- return res, expl
1100
-
1101
- def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]:
1102
- self.push_format_context()
1103
- # We first check if we have overwritten a variable in the previous assert
1104
- match comp.left:
1105
- case ast.Name(id=name_id) if name_id in self.variables_overwrite.get(
1106
- self.scope, {}
1107
- ):
1108
- comp.left = self.variables_overwrite[self.scope][name_id] # type: ignore[assignment]
1109
- case ast.NamedExpr(target=ast.Name(id=target_id)):
1110
- self.variables_overwrite[self.scope][target_id] = comp.left # type: ignore[assignment]
1111
- left_res, left_expl = self.visit(comp.left)
1112
- if isinstance(comp.left, ast.Compare | ast.BoolOp):
1113
- left_expl = f"({left_expl})"
1114
- res_variables = [self.variable() for i in range(len(comp.ops))]
1115
- load_names: list[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables]
1116
- store_names = [ast.Name(v, ast.Store()) for v in res_variables]
1117
- it = zip(range(len(comp.ops)), comp.ops, comp.comparators, strict=True)
1118
- expls: list[ast.expr] = []
1119
- syms: list[ast.expr] = []
1120
- results = [left_res]
1121
- for i, op, next_operand in it:
1122
- match (next_operand, left_res):
1123
- case (
1124
- ast.NamedExpr(target=ast.Name(id=target_id)),
1125
- ast.Name(id=name_id),
1126
- ) if target_id == name_id:
1127
- next_operand.target.id = self.variable()
1128
- self.variables_overwrite[self.scope][name_id] = next_operand # type: ignore[assignment]
1129
-
1130
- next_res, next_expl = self.visit(next_operand)
1131
- if isinstance(next_operand, ast.Compare | ast.BoolOp):
1132
- next_expl = f"({next_expl})"
1133
- results.append(next_res)
1134
- sym = BINOP_MAP[op.__class__]
1135
- syms.append(ast.Constant(sym))
1136
- expl = f"{left_expl} {sym} {next_expl}"
1137
- expls.append(ast.Constant(expl))
1138
- res_expr = ast.copy_location(ast.Compare(left_res, [op], [next_res]), comp)
1139
- self.statements.append(ast.Assign([store_names[i]], res_expr))
1140
- left_res, left_expl = next_res, next_expl
1141
- # Use pytest.assertion.util._reprcompare if that's available.
1142
- expl_call = self.helper(
1143
- "_call_reprcompare",
1144
- ast.Tuple(syms, ast.Load()),
1145
- ast.Tuple(load_names, ast.Load()),
1146
- ast.Tuple(expls, ast.Load()),
1147
- ast.Tuple(results, ast.Load()),
1148
- )
1149
- if len(comp.ops) > 1:
1150
- res: ast.expr = ast.BoolOp(ast.And(), load_names)
1151
- else:
1152
- res = load_names[0]
1153
-
1154
- return res, self.explanation_param(self.pop_format_context(expl_call))
1155
-
1156
-
1157
- def try_makedirs(cache_dir: Path) -> bool:
1158
- """Attempt to create the given directory and sub-directories exist.
1159
-
1160
- Returns True if successful or if it already exists.
1161
- """
1162
- try:
1163
- os.makedirs(cache_dir, exist_ok=True)
1164
- except (FileNotFoundError, NotADirectoryError, FileExistsError):
1165
- # One of the path components was not a directory:
1166
- # - we're in a zip file
1167
- # - it is a file
1168
- return False
1169
- except PermissionError:
1170
- return False
1171
- except OSError as e:
1172
- # as of now, EROFS doesn't have an equivalent OSError-subclass
1173
- #
1174
- # squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not
1175
- # implemented" for a read-only error
1176
- if e.errno in {errno.EROFS, errno.ENOSYS}:
1177
- return False
1178
- raise
1179
- return True
1180
-
1181
-
1182
- def get_cache_dir(file_path: Path) -> Path:
1183
- """Return the cache directory to write .pyc files for the given .py file path."""
1184
- if sys.pycache_prefix:
1185
- # given:
1186
- # prefix = '/tmp/pycs'
1187
- # path = '/home/user/proj/test_app.py'
1188
- # we want:
1189
- # '/tmp/pycs/home/user/proj'
1190
- return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1])
1191
- else:
1192
- # classic pycache directory
1193
- return file_path.parent / "__pycache__"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/truncate.py DELETED
@@ -1,128 +0,0 @@
1
- """Utilities for truncating assertion output.
2
-
3
- Current default behaviour is to truncate assertion explanations at
4
- terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from _pytest.compat import running_on_ci
10
- from _pytest.config import Config
11
- from _pytest.nodes import Item
12
-
13
-
14
- DEFAULT_MAX_LINES = 8
15
- DEFAULT_MAX_CHARS = DEFAULT_MAX_LINES * 80
16
- USAGE_MSG = "use '-vv' to show"
17
-
18
-
19
- def truncate_if_required(explanation: list[str], item: Item) -> list[str]:
20
- """Truncate this assertion explanation if the given test item is eligible."""
21
- should_truncate, max_lines, max_chars = _get_truncation_parameters(item)
22
- if should_truncate:
23
- return _truncate_explanation(
24
- explanation,
25
- max_lines=max_lines,
26
- max_chars=max_chars,
27
- )
28
- return explanation
29
-
30
-
31
- def _get_truncation_parameters(item: Item) -> tuple[bool, int, int]:
32
- """Return the truncation parameters related to the given item, as (should truncate, max lines, max chars)."""
33
- # We do not need to truncate if one of conditions is met:
34
- # 1. Verbosity level is 2 or more;
35
- # 2. Test is being run in CI environment;
36
- # 3. Both truncation_limit_lines and truncation_limit_chars
37
- # .ini parameters are set to 0 explicitly.
38
- max_lines = item.config.getini("truncation_limit_lines")
39
- max_lines = int(max_lines if max_lines is not None else DEFAULT_MAX_LINES)
40
-
41
- max_chars = item.config.getini("truncation_limit_chars")
42
- max_chars = int(max_chars if max_chars is not None else DEFAULT_MAX_CHARS)
43
-
44
- verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
45
-
46
- should_truncate = verbose < 2 and not running_on_ci()
47
- should_truncate = should_truncate and (max_lines > 0 or max_chars > 0)
48
-
49
- return should_truncate, max_lines, max_chars
50
-
51
-
52
- def _truncate_explanation(
53
- input_lines: list[str],
54
- max_lines: int,
55
- max_chars: int,
56
- ) -> list[str]:
57
- """Truncate given list of strings that makes up the assertion explanation.
58
-
59
- Truncates to either max_lines, or max_chars - whichever the input reaches
60
- first, taking the truncation explanation into account. The remaining lines
61
- will be replaced by a usage message.
62
-
63
- If max_chars=0, no truncation by character count is performed.
64
- If max_lines=0, no truncation by line count is performed.
65
-
66
- When this function is launched we know max_lines > 0 or max_chars > 0
67
- because _get_truncation_parameters was called first.
68
- """
69
- # The length of the truncation explanation depends on the number of lines
70
- # removed but is at least 68 characters:
71
- # The real value is
72
- # 64 (for the base message:
73
- # '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
74
- # )
75
- # + 1 (for plural)
76
- # + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
77
- # + 3 for the '...' added to the truncated line
78
- # But if there's more than 100 lines it's very likely that we're going to
79
- # truncate, so we don't need the exact value using log10.
80
- tolerable_max_chars = (
81
- max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
82
- )
83
- # The truncation explanation add two lines to the output
84
- if max_lines == 0 or len(input_lines) <= max_lines + 2:
85
- if max_chars == 0 or sum(len(s) for s in input_lines) <= tolerable_max_chars:
86
- return input_lines
87
- truncated_explanation = input_lines
88
- else:
89
- # Truncate first to max_lines, and then truncate to max_chars if necessary
90
- truncated_explanation = input_lines[:max_lines]
91
- # We reevaluate the need to truncate chars following removal of some lines
92
- need_to_truncate_char = (
93
- max_chars > 0
94
- and sum(len(e) for e in truncated_explanation) > tolerable_max_chars
95
- )
96
- if need_to_truncate_char:
97
- truncated_explanation = _truncate_by_char_count(
98
- truncated_explanation, max_chars
99
- )
100
- # Something was truncated, adding '...' at the end to show that
101
- truncated_explanation[-1] += "..."
102
- truncated_line_count = (
103
- len(input_lines) - len(truncated_explanation) + int(need_to_truncate_char)
104
- )
105
- return [
106
- *truncated_explanation,
107
- "",
108
- f"...Full output truncated ({truncated_line_count} line"
109
- f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
110
- ]
111
-
112
-
113
- def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]:
114
- # Find point at which input length exceeds total allowed length
115
- iterated_char_count = 0
116
- for iterated_index, input_line in enumerate(input_lines):
117
- if iterated_char_count + len(input_line) > max_chars:
118
- break
119
- iterated_char_count += len(input_line)
120
-
121
- # Create truncated explanation with modified final line
122
- truncated_result = input_lines[:iterated_index]
123
- final_line = input_lines[iterated_index]
124
- if final_line:
125
- final_line_truncate_point = max_chars - iterated_char_count
126
- final_line = final_line[:final_line_truncate_point]
127
- truncated_result.append(final_line)
128
- return truncated_result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/assertion/util.py DELETED
@@ -1,215 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- """Utilities for assertion debugging."""
3
-
4
- from __future__ import annotations
5
-
6
- from collections.abc import Callable
7
- from collections.abc import Iterator
8
- from collections.abc import Sequence
9
- from typing import Literal
10
- from unicodedata import normalize
11
-
12
- from _pytest import outcomes
13
- import _pytest._code
14
- from _pytest._io.saferepr import saferepr
15
- from _pytest._io.saferepr import saferepr_unlimited
16
- from _pytest.assertion._compare_any import _compare_eq_any
17
- from _pytest.assertion._compare_set import SET_COMPARISON_FUNCTIONS
18
- from _pytest.assertion._guards import isset
19
- from _pytest.assertion._guards import istext
20
- from _pytest.assertion._typing import _AssertionTextDiffStyle
21
- from _pytest.assertion._typing import _HighlightFunc
22
- from _pytest.assertion.compare_text import _notin_text
23
- from _pytest.assertion.highlight import dummy_highlighter as dummy_highlighter
24
- from _pytest.config import Config
25
- from _pytest.config import UsageError
26
-
27
-
28
- # The _reprcompare attribute on the util module is used by the new assertion
29
- # interpretation code and assertion rewriter to detect this plugin was
30
- # loaded and in turn call the hooks defined here as part of the
31
- # DebugInterpreter.
32
- _reprcompare: Callable[[str, object, object], str | None] | None = None
33
-
34
- # Works similarly as _reprcompare attribute. Is populated with the hook call
35
- # when pytest_runtest_setup is called.
36
- _assertion_pass: Callable[[int, str, str], None] | None = None
37
-
38
- # Config object which is assigned during pytest_runtest_protocol.
39
- _config: Config | None = None
40
-
41
- ASSERTION_TEXT_DIFF_STYLE_INI = "assertion_text_diff_style"
42
- ASSERTION_TEXT_DIFF_STYLE_NDIFF: Literal["ndiff"] = "ndiff"
43
- ASSERTION_TEXT_DIFF_STYLE_BLOCK: Literal["block"] = "block"
44
- ASSERTION_TEXT_DIFF_STYLE_CHOICES = (
45
- ASSERTION_TEXT_DIFF_STYLE_NDIFF,
46
- ASSERTION_TEXT_DIFF_STYLE_BLOCK,
47
- )
48
-
49
-
50
- def get_assertion_text_diff_style(config: Config) -> _AssertionTextDiffStyle:
51
- style = str(config.getini(ASSERTION_TEXT_DIFF_STYLE_INI))
52
- match style:
53
- case "ndiff" | "block":
54
- return style
55
- case _:
56
- choices = ", ".join(
57
- repr(choice) for choice in ASSERTION_TEXT_DIFF_STYLE_CHOICES
58
- )
59
- raise UsageError(
60
- f"{ASSERTION_TEXT_DIFF_STYLE_INI} must be one of {choices}; got {style!r}"
61
- )
62
-
63
-
64
- def validate_assertion_text_diff_style(config: Config) -> None:
65
- get_assertion_text_diff_style(config)
66
-
67
-
68
- def format_explanation(explanation: str) -> str:
69
- r"""Format an explanation.
70
-
71
- Normally all embedded newlines are escaped, however there are
72
- three exceptions: \n{, \n} and \n~. The first two are intended
73
- cover nested explanations, see function and attribute explanations
74
- for examples (.visit_Call(), visit_Attribute()). The last one is
75
- for when one explanation needs to span multiple lines, e.g. when
76
- displaying diffs.
77
- """
78
- lines = _split_explanation(explanation)
79
- result = _format_lines(lines)
80
- return "\n".join(result)
81
-
82
-
83
- def _split_explanation(explanation: str) -> list[str]:
84
- r"""Return a list of individual lines in the explanation.
85
-
86
- This will return a list of lines split on '\n{', '\n}' and '\n~'.
87
- Any other newlines will be escaped and appear in the line as the
88
- literal '\n' characters.
89
- """
90
- raw_lines = (explanation or "").split("\n")
91
- lines = [raw_lines[0]]
92
- for values in raw_lines[1:]:
93
- if values and values[0] in ["{", "}", "~", ">"]:
94
- lines.append(values)
95
- else:
96
- lines[-1] += "\\n" + values
97
- return lines
98
-
99
-
100
- def _format_lines(lines: Sequence[str]) -> list[str]:
101
- """Format the individual lines.
102
-
103
- This will replace the '{', '}' and '~' characters of our mini formatting
104
- language with the proper 'where ...', 'and ...' and ' + ...' text, taking
105
- care of indentation along the way.
106
-
107
- Return a list of formatted lines.
108
- """
109
- result = list(lines[:1])
110
- stack = [0]
111
- stackcnt = [0]
112
- for line in lines[1:]:
113
- if line.startswith("{"):
114
- if stackcnt[-1]:
115
- s = "and "
116
- else:
117
- s = "where "
118
- stack.append(len(result))
119
- stackcnt[-1] += 1
120
- stackcnt.append(0)
121
- result.append(" +" + " " * (len(stack) - 1) + s + line[1:])
122
- elif line.startswith("}"):
123
- stack.pop()
124
- stackcnt.pop()
125
- result[stack[-1]] += line[1:]
126
- else:
127
- assert line[0] in ["~", ">"]
128
- stack[-1] += 1
129
- indent = len(stack) if line.startswith("~") else len(stack) - 1
130
- result.append(" " * indent + line[1:])
131
- assert len(stack) == 1
132
- return result
133
-
134
-
135
- def assertrepr_compare(
136
- op: str,
137
- left: object,
138
- right: object,
139
- *,
140
- verbose: int,
141
- highlighter: _HighlightFunc,
142
- assertion_text_diff_style: _AssertionTextDiffStyle,
143
- ) -> Iterator[str]:
144
- """Yield specialised explanations for some operators/operands.
145
-
146
- The first line yielded is always the summary (``left op right``);
147
- subsequent lines are the detailed explanation. Yields nothing when no
148
- specialised explanation applies, which lets consumers map an empty
149
- iterator to "no explanation" without materialising anything.
150
-
151
- The iterator is lazy on purpose: a streaming consumer can stop pulling
152
- lines as soon as it has enough to show, so an enormous diff doesn't
153
- have to be built in full just to be thrown away.
154
- """
155
- # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
156
- # See issue #3246.
157
- use_ascii = (
158
- isinstance(left, str)
159
- and isinstance(right, str)
160
- and normalize("NFD", left) == normalize("NFD", right)
161
- )
162
-
163
- if verbose > 1:
164
- left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
165
- right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
166
- else:
167
- # XXX: "15 chars indentation" is wrong
168
- # ("E AssertionError: assert "); should use term width.
169
- maxsize = (
170
- 80 - 15 - len(op) - 2
171
- ) // 2 # 15 chars indentation, 1 space around op
172
-
173
- left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
174
- right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
175
-
176
- summary = f"{left_repr} {op} {right_repr}"
177
-
178
- try:
179
- if op == "==":
180
- source = _compare_eq_any(
181
- left,
182
- right,
183
- highlighter,
184
- verbose,
185
- assertion_text_diff_style,
186
- )
187
- elif op == "not in" and istext(left) and istext(right):
188
- source = _notin_text(left, right, verbose)
189
- elif op in {"!=", ">=", "<=", ">", "<"} and isset(left) and isset(right):
190
- source = SET_COMPARISON_FUNCTIONS[op](left, right, highlighter, verbose)
191
- else:
192
- source = iter(())
193
-
194
- # Only yield the summary if there is a detailed explanation.
195
- # Make sure there's a separating empty line after the summary.
196
- summary_yielded = False
197
- for line in source:
198
- if not summary_yielded:
199
- yield summary
200
- if line != "":
201
- yield ""
202
- summary_yielded = True
203
- yield line
204
- except outcomes.Exit:
205
- raise
206
- except Exception:
207
- repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
208
- if not summary_yielded:
209
- yield summary
210
- yield ""
211
- summary_yielded = True
212
- yield (
213
- f"(pytest_assertion plugin: representation of details failed: {repr_crash}."
214
- )
215
- yield " Probably an object has a faulty __repr__.)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/cacheprovider.py DELETED
@@ -1,640 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- """Implementation of the cache provider."""
3
-
4
- # This plugin was not named "cache" to avoid conflicts with the external
5
- # pytest-cache version.
6
- from __future__ import annotations
7
-
8
- from collections.abc import Generator
9
- from collections.abc import Iterable
10
- import dataclasses
11
- import errno
12
- import json
13
- import os
14
- from pathlib import Path
15
- import shutil
16
- import tempfile
17
- from typing import final
18
-
19
- from .pathlib import resolve_from_str
20
- from .pathlib import rm_rf
21
- from .reports import CollectReport
22
- from _pytest import nodes
23
- from _pytest._io import TerminalWriter
24
- from _pytest.config import Config
25
- from _pytest.config import ExitCode
26
- from _pytest.config import hookimpl
27
- from _pytest.config.argparsing import Parser
28
- from _pytest.deprecated import check_ispytest
29
- from _pytest.fixtures import fixture
30
- from _pytest.fixtures import FixtureRequest
31
- from _pytest.main import Session
32
- from _pytest.nodes import Directory
33
- from _pytest.nodes import File
34
- from _pytest.reports import TestReport
35
-
36
-
37
- CACHEDIR_FILES: dict[str, bytes] = {
38
- "README.md": b"""\
39
- # pytest cache directory #
40
-
41
- This directory contains data from the pytest's cache plugin,
42
- which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
43
-
44
- **Do not** commit this to version control.
45
-
46
- See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
47
- """,
48
- ".gitignore": b"# Created by pytest automatically.\n*\n",
49
- "CACHEDIR.TAG": b"""\
50
- Signature: 8a477f597d28d172789f06886806bc55
51
- # This file is a cache directory tag created by pytest.
52
- # For information about cache directory tags, see:
53
- # https://bford.info/cachedir/spec.html
54
- """,
55
- }
56
-
57
-
58
- def _make_cachedir(target: Path) -> None:
59
- """Create the pytest cache directory atomically with supporting files.
60
-
61
- Creates a temporary directory with README.md, .gitignore, and CACHEDIR.TAG,
62
- then atomically renames it to the target location. If another process wins
63
- the race, the temporary directory is cleaned up.
64
- """
65
- target.parent.mkdir(parents=True, exist_ok=True)
66
- path = Path(tempfile.mkdtemp(prefix="pytest-cache-files-", dir=target.parent))
67
- try:
68
- # Reset permissions to the default, see #12308.
69
- # Note: there's no way to get the current umask atomically, eek.
70
- umask = os.umask(0o022)
71
- os.umask(umask)
72
- path.chmod(0o777 - umask)
73
-
74
- for name, content in CACHEDIR_FILES.items():
75
- path.joinpath(name).write_bytes(content)
76
-
77
- path.rename(target)
78
- except OSError as e:
79
- # If 2 concurrent pytests both race to the rename, the loser
80
- # gets "Directory not empty" from the rename. In this case,
81
- # everything is handled so just continue after cleanup.
82
- # On Windows, the error is a FileExistsError which translates to EEXIST.
83
- if e.errno not in (errno.ENOTEMPTY, errno.EEXIST):
84
- raise
85
- finally:
86
- shutil.rmtree(path, ignore_errors=True)
87
-
88
-
89
- @final
90
- @dataclasses.dataclass
91
- class Cache:
92
- """Instance of the `cache` fixture."""
93
-
94
- _cachedir: Path = dataclasses.field(repr=False)
95
- _config: Config = dataclasses.field(repr=False)
96
-
97
- # Sub-directory under cache-dir for directories created by `mkdir()`.
98
- _CACHE_PREFIX_DIRS = "d"
99
-
100
- # Sub-directory under cache-dir for values created by `set()`.
101
- _CACHE_PREFIX_VALUES = "v"
102
-
103
- def __init__(
104
- self, cachedir: Path, config: Config, *, _ispytest: bool = False
105
- ) -> None:
106
- check_ispytest(_ispytest)
107
- self._cachedir = cachedir
108
- self._config = config
109
-
110
- @classmethod
111
- def for_config(cls, config: Config, *, _ispytest: bool = False) -> Cache:
112
- """Create the Cache instance for a Config.
113
-
114
- :meta private:
115
- """
116
- check_ispytest(_ispytest)
117
- cachedir = cls.cache_dir_from_config(config, _ispytest=True)
118
- if config.getoption("cacheclear") and cachedir.is_dir():
119
- cls.clear_cache(cachedir, _ispytest=True)
120
- return cls(cachedir, config, _ispytest=True)
121
-
122
- @classmethod
123
- def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None:
124
- """Clear the sub-directories used to hold cached directories and values.
125
-
126
- :meta private:
127
- """
128
- check_ispytest(_ispytest)
129
- for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
130
- d = cachedir / prefix
131
- if d.is_dir():
132
- rm_rf(d)
133
-
134
- @staticmethod
135
- def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path:
136
- """Get the path to the cache directory for a Config.
137
-
138
- :meta private:
139
- """
140
- check_ispytest(_ispytest)
141
- return resolve_from_str(config.getini("cache_dir"), config.rootpath)
142
-
143
- def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
144
- """Issue a cache warning.
145
-
146
- :meta private:
147
- """
148
- check_ispytest(_ispytest)
149
- import warnings
150
-
151
- from _pytest.warning_types import PytestCacheWarning
152
-
153
- warnings.warn(
154
- PytestCacheWarning(fmt.format(**args) if args else fmt),
155
- self._config.hook,
156
- stacklevel=3,
157
- )
158
-
159
- def _mkdir(self, path: Path) -> None:
160
- self._ensure_cache_dir_and_supporting_files()
161
- path.mkdir(exist_ok=True, parents=True)
162
-
163
- def mkdir(self, name: str) -> Path:
164
- """Return a directory path object with the given name.
165
-
166
- If the directory does not yet exist, it will be created. You can use
167
- it to manage files to e.g. store/retrieve database dumps across test
168
- sessions.
169
-
170
- .. versionadded:: 7.0
171
-
172
- :param name:
173
- Must be a string not containing a ``/`` separator.
174
- Make sure the name contains your plugin or application
175
- identifiers to prevent clashes with other cache users.
176
- """
177
- path = Path(name)
178
- if len(path.parts) > 1:
179
- raise ValueError("name is not allowed to contain path separators")
180
- res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
181
- self._mkdir(res)
182
- return res
183
-
184
- def _getvaluepath(self, key: str) -> Path:
185
- return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
186
-
187
- def get(self, key: str, default):
188
- """Return the cached value for the given key.
189
-
190
- If no value was yet cached or the value cannot be read, the specified
191
- default is returned.
192
-
193
- :param key:
194
- Must be a ``/`` separated value. Usually the first
195
- name is the name of your plugin or your application.
196
- :param default:
197
- The value to return in case of a cache-miss or invalid cache value.
198
- """
199
- path = self._getvaluepath(key)
200
- try:
201
- with path.open("r", encoding="UTF-8") as f:
202
- return json.load(f)
203
- except (ValueError, OSError):
204
- return default
205
-
206
- def set(self, key: str, value: object) -> None:
207
- """Save value for the given key.
208
-
209
- :param key:
210
- Must be a ``/`` separated value. Usually the first
211
- name is the name of your plugin or your application.
212
- :param value:
213
- Must be of any combination of basic python types,
214
- including nested types like lists of dictionaries.
215
- """
216
- path = self._getvaluepath(key)
217
- try:
218
- self._mkdir(path.parent)
219
- except OSError as exc:
220
- self.warn(
221
- f"could not create cache path {path}: {exc}",
222
- _ispytest=True,
223
- )
224
- return
225
- data = json.dumps(value, ensure_ascii=False, indent=2)
226
- try:
227
- f = path.open("w", encoding="UTF-8")
228
- except OSError as exc:
229
- self.warn(
230
- f"cache could not write path {path}: {exc}",
231
- _ispytest=True,
232
- )
233
- else:
234
- with f:
235
- f.write(data)
236
-
237
- def _ensure_cache_dir_and_supporting_files(self) -> None:
238
- """Create the cache dir and its supporting files."""
239
- if not self._cachedir.is_dir():
240
- _make_cachedir(self._cachedir)
241
-
242
-
243
- class LFPluginCollWrapper:
244
- def __init__(self, lfplugin: LFPlugin) -> None:
245
- self.lfplugin = lfplugin
246
- self._collected_at_least_one_failure = False
247
-
248
- @hookimpl(wrapper=True)
249
- def pytest_make_collect_report(
250
- self, collector: nodes.Collector
251
- ) -> Generator[None, CollectReport, CollectReport]:
252
- res = yield
253
- if isinstance(collector, Session | Directory):
254
- # Sort any lf-paths to the beginning.
255
- lf_paths = self.lfplugin._last_failed_paths
256
-
257
- # Use stable sort to prioritize last failed.
258
- def sort_key(node: nodes.Item | nodes.Collector) -> bool:
259
- return node.path in lf_paths
260
-
261
- res.result = sorted(
262
- res.result,
263
- key=sort_key,
264
- reverse=True,
265
- )
266
-
267
- elif isinstance(collector, File):
268
- if collector.path in self.lfplugin._last_failed_paths:
269
- result = res.result
270
- lastfailed = self.lfplugin.lastfailed
271
-
272
- # Only filter with known failures.
273
- if not self._collected_at_least_one_failure:
274
- if not any(x.nodeid in lastfailed for x in result):
275
- return res
276
- self.lfplugin.config.pluginmanager.register(
277
- LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
278
- )
279
- self._collected_at_least_one_failure = True
280
-
281
- session = collector.session
282
- result[:] = [
283
- x
284
- for x in result
285
- if x.nodeid in lastfailed
286
- # Include any passed arguments (not trivial to filter).
287
- or session.isinitpath(x.path)
288
- # Keep all sub-collectors.
289
- or isinstance(x, nodes.Collector)
290
- ]
291
-
292
- return res
293
-
294
-
295
- class LFPluginCollSkipfiles:
296
- def __init__(self, lfplugin: LFPlugin) -> None:
297
- self.lfplugin = lfplugin
298
-
299
- @hookimpl
300
- def pytest_make_collect_report(
301
- self, collector: nodes.Collector
302
- ) -> CollectReport | None:
303
- if isinstance(collector, File):
304
- if collector.path not in self.lfplugin._last_failed_paths:
305
- self.lfplugin._skipped_files += 1
306
-
307
- return CollectReport(
308
- collector.nodeid, "passed", longrepr=None, result=[]
309
- )
310
- return None
311
-
312
-
313
- class LFPlugin:
314
- """Plugin which implements the --lf (run last-failing) option."""
315
-
316
- def __init__(self, config: Config) -> None:
317
- self.config = config
318
- active_keys = "lf", "failedfirst"
319
- self.active = any(config.getoption(key) for key in active_keys)
320
- assert config.cache
321
- self.lastfailed: dict[str, bool] = config.cache.get("cache/lastfailed", {})
322
- self._previously_failed_count: int | None = None
323
- self._report_status: str | None = None
324
- self._skipped_files = 0 # count skipped files during collection due to --lf
325
-
326
- if config.getoption("lf"):
327
- self._last_failed_paths = self.get_last_failed_paths()
328
- config.pluginmanager.register(
329
- LFPluginCollWrapper(self), "lfplugin-collwrapper"
330
- )
331
-
332
- def get_last_failed_paths(self) -> set[Path]:
333
- """Return a set with all Paths of the previously failed nodeids and
334
- their parents."""
335
- rootpath = self.config.rootpath
336
- result = set()
337
- for nodeid in self.lastfailed:
338
- path = rootpath / nodeid.split("::")[0]
339
- result.add(path)
340
- result.update(path.parents)
341
- return {x for x in result if x.exists()}
342
-
343
- def pytest_report_collectionfinish(self) -> str | None:
344
- if self.active and self.config.get_verbosity() >= 0:
345
- return f"run-last-failure: {self._report_status}"
346
- return None
347
-
348
- def pytest_runtest_logreport(self, report: TestReport) -> None:
349
- if (report.when == "call" and report.passed) or report.skipped:
350
- self.lastfailed.pop(report.nodeid, None)
351
- elif report.failed:
352
- self.lastfailed[report.nodeid] = True
353
-
354
- def pytest_collectreport(self, report: CollectReport) -> None:
355
- passed = report.outcome in ("passed", "skipped")
356
- if passed:
357
- if report.nodeid in self.lastfailed:
358
- self.lastfailed.pop(report.nodeid)
359
- self.lastfailed.update((item.nodeid, True) for item in report.result)
360
- else:
361
- self.lastfailed[report.nodeid] = True
362
-
363
- @hookimpl(wrapper=True, tryfirst=True)
364
- def pytest_collection_modifyitems(
365
- self, config: Config, items: list[nodes.Item]
366
- ) -> Generator[None]:
367
- res = yield
368
-
369
- if not self.active:
370
- return res
371
-
372
- if self.lastfailed:
373
- previously_failed = []
374
- previously_passed = []
375
- for item in items:
376
- if item.nodeid in self.lastfailed:
377
- previously_failed.append(item)
378
- else:
379
- previously_passed.append(item)
380
- self._previously_failed_count = len(previously_failed)
381
-
382
- if not previously_failed:
383
- # Running a subset of all tests with recorded failures
384
- # only outside of it.
385
- self._report_status = (
386
- f"{len(self.lastfailed)} known failures not in selected tests"
387
- )
388
- else:
389
- if self.config.getoption("lf"):
390
- items[:] = previously_failed
391
- config.hook.pytest_deselected(items=previously_passed)
392
- else: # --failedfirst
393
- items[:] = previously_failed + previously_passed
394
-
395
- noun = "failure" if self._previously_failed_count == 1 else "failures"
396
- suffix = " first" if self.config.getoption("failedfirst") else ""
397
- self._report_status = (
398
- f"rerun previous {self._previously_failed_count} {noun}{suffix}"
399
- )
400
-
401
- if self._skipped_files > 0:
402
- files_noun = "file" if self._skipped_files == 1 else "files"
403
- self._report_status += f" (skipped {self._skipped_files} {files_noun})"
404
- else:
405
- self._report_status = "no previously failed tests, "
406
- if self.config.getoption("last_failed_no_failures") == "none":
407
- self._report_status += "deselecting all items."
408
- config.hook.pytest_deselected(items=items[:])
409
- items[:] = []
410
- else:
411
- self._report_status += "not deselecting items."
412
-
413
- return res
414
-
415
- def pytest_sessionfinish(self, session: Session) -> None:
416
- config = self.config
417
- if config.getoption("cacheshow") or hasattr(config, "workerinput"):
418
- return
419
-
420
- assert config.cache is not None
421
- saved_lastfailed = config.cache.get("cache/lastfailed", {})
422
- if saved_lastfailed != self.lastfailed:
423
- config.cache.set("cache/lastfailed", self.lastfailed)
424
-
425
-
426
- class NFPlugin:
427
- """Plugin which implements the --nf (run new-first) option."""
428
-
429
- def __init__(self, config: Config) -> None:
430
- self.config = config
431
- self.active = config.option.newfirst
432
- assert config.cache is not None
433
- self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
434
-
435
- @hookimpl(wrapper=True, tryfirst=True)
436
- def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> Generator[None]:
437
- res = yield
438
-
439
- if self.active:
440
- new_items: dict[str, nodes.Item] = {}
441
- other_items: dict[str, nodes.Item] = {}
442
- for item in items:
443
- if item.nodeid not in self.cached_nodeids:
444
- new_items[item.nodeid] = item
445
- else:
446
- other_items[item.nodeid] = item
447
-
448
- items[:] = self._get_increasing_order(
449
- new_items.values()
450
- ) + self._get_increasing_order(other_items.values())
451
- self.cached_nodeids.update(new_items)
452
- else:
453
- self.cached_nodeids.update(item.nodeid for item in items)
454
-
455
- return res
456
-
457
- def _get_increasing_order(self, items: Iterable[nodes.Item]) -> list[nodes.Item]:
458
- return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True)
459
-
460
- def pytest_sessionfinish(self) -> None:
461
- config = self.config
462
- if config.getoption("cacheshow") or hasattr(config, "workerinput"):
463
- return
464
-
465
- if config.getoption("collectonly"):
466
- return
467
-
468
- assert config.cache is not None
469
- config.cache.set("cache/nodeids", sorted(self.cached_nodeids))
470
-
471
-
472
- def pytest_addoption(parser: Parser) -> None:
473
- """Add command-line options for cache functionality.
474
-
475
- :param parser: Parser object to add command-line options to.
476
- """
477
- group = parser.getgroup("general")
478
- group.addoption(
479
- "--lf",
480
- "--last-failed",
481
- action="store_true",
482
- dest="lf",
483
- help="Rerun only the tests that failed at the last run (or all if none failed)",
484
- )
485
- group.addoption(
486
- "--ff",
487
- "--failed-first",
488
- action="store_true",
489
- dest="failedfirst",
490
- help="Run all tests, but run the last failures first. "
491
- "This may re-order tests and thus lead to "
492
- "repeated fixture setup/teardown.",
493
- )
494
- group.addoption(
495
- "--nf",
496
- "--new-first",
497
- action="store_true",
498
- dest="newfirst",
499
- help="Run tests from new files first, then the rest of the tests "
500
- "sorted by file mtime",
501
- )
502
- group.addoption(
503
- "--cache-show",
504
- action="append",
505
- nargs="?",
506
- dest="cacheshow",
507
- help=(
508
- "Show cache contents, don't perform collection or tests. "
509
- "Optional argument: glob (default: '*')."
510
- ),
511
- )
512
- group.addoption(
513
- "--cache-clear",
514
- action="store_true",
515
- dest="cacheclear",
516
- help="Remove all cache contents at start of test run",
517
- )
518
- cache_dir_default = ".pytest_cache"
519
- if "TOX_ENV_DIR" in os.environ:
520
- cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default)
521
- parser.addini("cache_dir", default=cache_dir_default, help="Cache directory path")
522
- group.addoption(
523
- "--lfnf",
524
- "--last-failed-no-failures",
525
- action="store",
526
- dest="last_failed_no_failures",
527
- choices=("all", "none"),
528
- default="all",
529
- help="With ``--lf``, determines whether to execute tests when there "
530
- "are no previously (known) failures or when no "
531
- "cached ``lastfailed`` data was found. "
532
- "``all`` (the default) runs the full test suite again. "
533
- "``none`` just emits a message about no known failures and exits successfully.",
534
- )
535
-
536
-
537
- def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
538
- if config.option.cacheshow and not config.option.help:
539
- from _pytest.main import wrap_session
540
-
541
- return wrap_session(config, cacheshow)
542
- return None
543
-
544
-
545
- @hookimpl(tryfirst=True)
546
- def pytest_configure(config: Config) -> None:
547
- """Configure cache system and register related plugins.
548
-
549
- Creates the Cache instance and registers the last-failed (LFPlugin)
550
- and new-first (NFPlugin) plugins with the plugin manager.
551
-
552
- :param config: pytest configuration object.
553
- """
554
- config.cache = Cache.for_config(config, _ispytest=True)
555
- config.pluginmanager.register(LFPlugin(config), "lfplugin")
556
- config.pluginmanager.register(NFPlugin(config), "nfplugin")
557
-
558
-
559
- @fixture
560
- def cache(request: FixtureRequest) -> Cache:
561
- """Return a cache object that can persist state between testing sessions.
562
-
563
- cache.get(key, default)
564
- cache.set(key, value)
565
-
566
- Keys must be ``/`` separated strings, where the first part is usually the
567
- name of your plugin or application to avoid clashes with other cache users.
568
-
569
- Values can be any object handled by the json stdlib module.
570
- """
571
- assert request.config.cache is not None
572
- return request.config.cache
573
-
574
-
575
- def pytest_report_header(config: Config) -> str | None:
576
- """Display cachedir with --cache-show and if non-default."""
577
- if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache":
578
- assert config.cache is not None
579
- cachedir = config.cache._cachedir
580
- # TODO: evaluate generating upward relative paths
581
- # starting with .., ../.. if sensible
582
-
583
- try:
584
- displaypath = cachedir.relative_to(config.rootpath)
585
- except ValueError:
586
- displaypath = cachedir
587
- return f"cachedir: {displaypath}"
588
- return None
589
-
590
-
591
- def cacheshow(config: Config, session: Session) -> int:
592
- """Display cache contents when --cache-show is used.
593
-
594
- Shows cached values and directories matching the specified glob pattern
595
- (default: '*'). Displays cache location, cached test results, and
596
- any cached directories created by plugins.
597
-
598
- :param config: pytest configuration object.
599
- :param session: pytest session object.
600
- :returns: Exit code (0 for success).
601
- """
602
- from pprint import pformat
603
-
604
- assert config.cache is not None
605
-
606
- tw = TerminalWriter()
607
- tw.line("cachedir: " + str(config.cache._cachedir))
608
- if not config.cache._cachedir.is_dir():
609
- tw.line("cache is empty")
610
- return 0
611
-
612
- glob = config.option.cacheshow[0]
613
- if glob is None:
614
- glob = "*"
615
-
616
- dummy = object()
617
- basedir = config.cache._cachedir
618
- vdir = basedir / Cache._CACHE_PREFIX_VALUES
619
- tw.sep("-", f"cache values for {glob!r}")
620
- for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()):
621
- key = str(valpath.relative_to(vdir))
622
- val = config.cache.get(key, dummy)
623
- if val is dummy:
624
- tw.line(f"{key} contains unreadable content, will be ignored")
625
- else:
626
- tw.line(f"{key} contains:")
627
- for line in pformat(val).splitlines():
628
- tw.line(" " + line)
629
-
630
- ddir = basedir / Cache._CACHE_PREFIX_DIRS
631
- if ddir.is_dir():
632
- contents = sorted(ddir.rglob(glob))
633
- tw.sep("-", f"cache directories for {glob!r}")
634
- for p in contents:
635
- # if p.is_dir():
636
- # print("%s/" % p.relative_to(basedir))
637
- if p.is_file():
638
- key = str(p.relative_to(basedir))
639
- tw.line(f"{key} is a file of length {p.stat().st_size}")
640
- return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/capture.py DELETED
@@ -1,1151 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- """Per-test stdout/stderr capturing mechanism."""
3
-
4
- from __future__ import annotations
5
-
6
- import abc
7
- import collections
8
- from collections.abc import Generator
9
- from collections.abc import Iterable
10
- from collections.abc import Iterator
11
- import contextlib
12
- import io
13
- from io import UnsupportedOperation
14
- import os
15
- import sys
16
- from tempfile import TemporaryFile
17
- from types import TracebackType
18
- from typing import Any
19
- from typing import AnyStr
20
- from typing import BinaryIO
21
- from typing import cast
22
- from typing import Final
23
- from typing import final
24
- from typing import Generic
25
- from typing import Literal
26
- from typing import NamedTuple
27
- from typing import TextIO
28
- from typing import TYPE_CHECKING
29
-
30
-
31
- if TYPE_CHECKING:
32
- from typing_extensions import Self
33
-
34
- from _pytest.config import Config
35
- from _pytest.config import hookimpl
36
- from _pytest.config.argparsing import Parser
37
- from _pytest.deprecated import check_ispytest
38
- from _pytest.fixtures import fixture
39
- from _pytest.fixtures import SubRequest
40
- from _pytest.nodes import Collector
41
- from _pytest.nodes import File
42
- from _pytest.nodes import Item
43
- from _pytest.reports import CollectReport
44
-
45
-
46
- _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
47
-
48
-
49
- def pytest_addoption(parser: Parser) -> None:
50
- group = parser.getgroup("general")
51
- group.addoption(
52
- "--capture",
53
- action="store",
54
- default="fd",
55
- metavar="method",
56
- choices=["fd", "sys", "no", "tee-sys"],
57
- help="Per-test capturing method: one of fd|sys|no|tee-sys",
58
- )
59
- group._addoption( # private to use reserved lower-case short option
60
- "-s",
61
- action="store_const",
62
- const="no",
63
- dest="capture",
64
- help="Shortcut for --capture=no",
65
- )
66
-
67
-
68
- def _colorama_workaround() -> None:
69
- """Ensure colorama is imported so that it attaches to the correct stdio
70
- handles on Windows.
71
-
72
- colorama uses the terminal on import time. So if something does the
73
- first import of colorama while I/O capture is active, colorama will
74
- fail in various ways.
75
- """
76
- if sys.platform.startswith("win32"):
77
- try:
78
- import colorama # noqa: F401
79
- except ImportError:
80
- pass
81
-
82
-
83
- def _readline_workaround() -> None:
84
- """Ensure readline is imported early so it attaches to the correct stdio handles.
85
-
86
- This isn't a problem with the default GNU readline implementation, but in
87
- some configurations, Python uses libedit instead (on macOS, and for prebuilt
88
- binaries such as used by uv).
89
-
90
- In theory this is only needed if readline.backend == "libedit", but the
91
- workaround consists of importing readline here, so we already worked around
92
- the issue by the time we could check if we need to.
93
- """
94
- try:
95
- import readline # noqa: F401
96
- except ImportError:
97
- pass
98
-
99
-
100
- def _windowsconsoleio_workaround(stream: TextIO) -> None:
101
- """Workaround for Windows Unicode console handling.
102
-
103
- Python 3.6 implemented Unicode console handling for Windows. This works
104
- by reading/writing to the raw console handle using
105
- ``{Read,Write}ConsoleW``.
106
-
107
- The problem is that we are going to ``dup2`` over the stdio file
108
- descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
109
- handles used by Python to write to the console. Though there is still some
110
- weirdness and the console handle seems to only be closed randomly and not
111
- on the first call to ``CloseHandle``, or maybe it gets reopened with the
112
- same handle value when we suspend capturing.
113
-
114
- The workaround in this case will reopen stdio with a different fd which
115
- also means a different handle by replicating the logic in
116
- "Py_lifecycle.c:initstdio/create_stdio".
117
-
118
- :param stream:
119
- In practice ``sys.stdout`` or ``sys.stderr``, but given
120
- here as parameter for unittesting purposes.
121
-
122
- See https://github.com/pytest-dev/py/issues/103.
123
- """
124
- if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):
125
- return
126
-
127
- # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
128
- if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore]
129
- return
130
-
131
- raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer
132
-
133
- if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore]
134
- return
135
-
136
- def _reopen_stdio(f, mode):
137
- if not hasattr(stream.buffer, "raw") and mode[0] == "w":
138
- buffering = 0
139
- else:
140
- buffering = -1
141
-
142
- return io.TextIOWrapper(
143
- open(os.dup(f.fileno()), mode, buffering),
144
- f.encoding,
145
- f.errors,
146
- f.newlines,
147
- f.line_buffering,
148
- )
149
-
150
- sys.stdin = _reopen_stdio(sys.stdin, "rb")
151
- sys.stdout = _reopen_stdio(sys.stdout, "wb")
152
- sys.stderr = _reopen_stdio(sys.stderr, "wb")
153
-
154
-
155
- @hookimpl(wrapper=True)
156
- def pytest_load_initial_conftests(early_config: Config) -> Generator[None]:
157
- ns = early_config.known_args_namespace
158
- if ns.capture == "fd":
159
- _windowsconsoleio_workaround(sys.stdout)
160
- _colorama_workaround()
161
- _readline_workaround()
162
- pluginmanager = early_config.pluginmanager
163
- capman = CaptureManager(ns.capture)
164
- pluginmanager.register(capman, "capturemanager")
165
-
166
- # Make sure that capturemanager is properly reset at final shutdown.
167
- early_config.add_cleanup(capman.stop_global_capturing)
168
-
169
- # Finally trigger conftest loading but while capturing (issue #93).
170
- capman.start_global_capturing()
171
- try:
172
- try:
173
- yield
174
- finally:
175
- capman.suspend_global_capture()
176
- except BaseException:
177
- out, err = capman.read_global_capture()
178
- sys.stdout.write(out)
179
- sys.stderr.write(err)
180
- raise
181
-
182
-
183
- # IO Helpers.
184
-
185
-
186
- class EncodedFile(io.TextIOWrapper):
187
- __slots__ = ()
188
-
189
- @property
190
- def name(self) -> str:
191
- # Ensure that file.name is a string. Workaround for a Python bug
192
- # fixed in >=3.7.4: https://bugs.python.org/issue36015
193
- return repr(self.buffer)
194
-
195
- @property
196
- def mode(self) -> str:
197
- # TextIOWrapper doesn't expose a mode, but at least some of our
198
- # tests check it.
199
- assert hasattr(self.buffer, "mode")
200
- return cast(str, self.buffer.mode.replace("b", ""))
201
-
202
-
203
- class CaptureIO(io.TextIOWrapper):
204
- def __init__(self) -> None:
205
- super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)
206
-
207
- def getvalue(self) -> str:
208
- assert isinstance(self.buffer, io.BytesIO)
209
- return self.buffer.getvalue().decode("UTF-8")
210
-
211
-
212
- class TeeCaptureIO(CaptureIO):
213
- def __init__(self, other: TextIO) -> None:
214
- self._other = other
215
- super().__init__()
216
-
217
- def write(self, s: str) -> int:
218
- super().write(s)
219
- return self._other.write(s)
220
-
221
-
222
- class DontReadFromInput(TextIO):
223
- @property
224
- def encoding(self) -> str:
225
- assert sys.__stdin__ is not None
226
- return sys.__stdin__.encoding
227
-
228
- def read(self, size: int = -1) -> str:
229
- raise OSError(
230
- "pytest: reading from stdin while output is captured! Consider using `-s`."
231
- )
232
-
233
- readline = read
234
-
235
- def __next__(self) -> str:
236
- return self.readline()
237
-
238
- def readlines(self, hint: int | None = -1) -> list[str]:
239
- raise OSError(
240
- "pytest: reading from stdin while output is captured! Consider using `-s`."
241
- )
242
-
243
- def __iter__(self) -> Iterator[str]:
244
- return self
245
-
246
- def fileno(self) -> int:
247
- raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")
248
-
249
- def flush(self) -> None:
250
- raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()")
251
-
252
- def isatty(self) -> bool:
253
- return False
254
-
255
- def close(self) -> None:
256
- pass
257
-
258
- def readable(self) -> bool:
259
- return False
260
-
261
- def seek(self, offset: int, whence: int = 0) -> int:
262
- raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
263
-
264
- def seekable(self) -> bool:
265
- return False
266
-
267
- def tell(self) -> int:
268
- raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
269
-
270
- def truncate(self, size: int | None = None) -> int:
271
- raise UnsupportedOperation("cannot truncate stdin")
272
-
273
- def write(self, data: str) -> int:
274
- raise UnsupportedOperation("cannot write to stdin")
275
-
276
- def writelines(self, lines: Iterable[str]) -> None:
277
- raise UnsupportedOperation("Cannot write to stdin")
278
-
279
- def writable(self) -> bool:
280
- return False
281
-
282
- def __enter__(self) -> Self:
283
- return self
284
-
285
- def __exit__(
286
- self,
287
- type: type[BaseException] | None,
288
- value: BaseException | None,
289
- traceback: TracebackType | None,
290
- ) -> None:
291
- pass
292
-
293
- @property
294
- def buffer(self) -> BinaryIO:
295
- # The str/bytes doesn't actually matter in this type, so OK to fake.
296
- return self # type: ignore[return-value]
297
-
298
-
299
- # Capture classes.
300
-
301
-
302
- class CaptureBase(abc.ABC, Generic[AnyStr]):
303
- EMPTY_BUFFER: AnyStr
304
-
305
- @abc.abstractmethod
306
- def __init__(self, fd: int) -> None:
307
- raise NotImplementedError()
308
-
309
- @abc.abstractmethod
310
- def start(self) -> None:
311
- raise NotImplementedError()
312
-
313
- @abc.abstractmethod
314
- def done(self) -> None:
315
- raise NotImplementedError()
316
-
317
- @abc.abstractmethod
318
- def suspend(self) -> None:
319
- raise NotImplementedError()
320
-
321
- @abc.abstractmethod
322
- def resume(self) -> None:
323
- raise NotImplementedError()
324
-
325
- @abc.abstractmethod
326
- def writeorg(self, data: AnyStr) -> None:
327
- raise NotImplementedError()
328
-
329
- @abc.abstractmethod
330
- def snap(self) -> AnyStr:
331
- raise NotImplementedError()
332
-
333
-
334
- patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
335
-
336
-
337
- class NoCapture(CaptureBase[str]):
338
- EMPTY_BUFFER = ""
339
-
340
- def __init__(self, fd: int) -> None:
341
- pass
342
-
343
- def start(self) -> None:
344
- pass
345
-
346
- def done(self) -> None:
347
- pass
348
-
349
- def suspend(self) -> None:
350
- pass
351
-
352
- def resume(self) -> None:
353
- pass
354
-
355
- def snap(self) -> str:
356
- return ""
357
-
358
- def writeorg(self, data: str) -> None:
359
- pass
360
-
361
-
362
- class SysCaptureBase(CaptureBase[AnyStr]):
363
- def __init__(
364
- self, fd: int, tmpfile: TextIO | None = None, *, tee: bool = False
365
- ) -> None:
366
- name = patchsysdict[fd]
367
- self._old: TextIO = getattr(sys, name)
368
- self.name = name
369
- if tmpfile is None:
370
- if name == "stdin":
371
- tmpfile = DontReadFromInput()
372
- else:
373
- tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old)
374
- self.tmpfile = tmpfile
375
- self._state = "initialized"
376
-
377
- def repr(self, class_name: str) -> str:
378
- return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
379
- class_name,
380
- self.name,
381
- (hasattr(self, "_old") and repr(self._old)) or "<UNSET>",
382
- self._state,
383
- self.tmpfile,
384
- )
385
-
386
- def __repr__(self) -> str:
387
- return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
388
- self.__class__.__name__,
389
- self.name,
390
- (hasattr(self, "_old") and repr(self._old)) or "<UNSET>",
391
- self._state,
392
- self.tmpfile,
393
- )
394
-
395
- def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
396
- assert self._state in states, (
397
- "cannot {} in state {!r}: expected one of {}".format(
398
- op, self._state, ", ".join(states)
399
- )
400
- )
401
-
402
- def start(self) -> None:
403
- self._assert_state("start", ("initialized",))
404
- setattr(sys, self.name, self.tmpfile)
405
- self._state = "started"
406
-
407
- def done(self) -> None:
408
- self._assert_state("done", ("initialized", "started", "suspended", "done"))
409
- if self._state == "done":
410
- return
411
- setattr(sys, self.name, self._old)
412
- del self._old
413
- self.tmpfile.close()
414
- self._state = "done"
415
-
416
- def suspend(self) -> None:
417
- self._assert_state("suspend", ("started", "suspended"))
418
- setattr(sys, self.name, self._old)
419
- self._state = "suspended"
420
-
421
- def resume(self) -> None:
422
- self._assert_state("resume", ("started", "suspended"))
423
- if self._state == "started":
424
- return
425
- setattr(sys, self.name, self.tmpfile)
426
- self._state = "started"
427
-
428
-
429
- class SysCaptureBinary(SysCaptureBase[bytes]):
430
- EMPTY_BUFFER = b""
431
-
432
- def snap(self) -> bytes:
433
- self._assert_state("snap", ("started", "suspended"))
434
- self.tmpfile.seek(0)
435
- res = self.tmpfile.buffer.read()
436
- self.tmpfile.seek(0)
437
- self.tmpfile.truncate()
438
- return res
439
-
440
- def writeorg(self, data: bytes) -> None:
441
- self._assert_state("writeorg", ("started", "suspended"))
442
- self._old.flush()
443
- self._old.buffer.write(data)
444
- self._old.buffer.flush()
445
-
446
-
447
- class SysCapture(SysCaptureBase[str]):
448
- EMPTY_BUFFER = ""
449
-
450
- def snap(self) -> str:
451
- self._assert_state("snap", ("started", "suspended"))
452
- assert isinstance(self.tmpfile, CaptureIO)
453
- res = self.tmpfile.getvalue()
454
- self.tmpfile.seek(0)
455
- self.tmpfile.truncate()
456
- return res
457
-
458
- def writeorg(self, data: str) -> None:
459
- self._assert_state("writeorg", ("started", "suspended"))
460
- self._old.write(data)
461
- self._old.flush()
462
-
463
-
464
- class FDCaptureBase(CaptureBase[AnyStr]):
465
- def __init__(self, targetfd: int) -> None:
466
- self.targetfd = targetfd
467
-
468
- try:
469
- os.fstat(targetfd)
470
- except OSError:
471
- # FD capturing is conceptually simple -- create a temporary file,
472
- # redirect the FD to it, redirect back when done. But when the
473
- # target FD is invalid it throws a wrench into this lovely scheme.
474
- #
475
- # Tests themselves shouldn't care if the FD is valid, FD capturing
476
- # should work regardless of external circumstances. So falling back
477
- # to just sys capturing is not a good option.
478
- #
479
- # Further complications are the need to support suspend() and the
480
- # possibility of FD reuse (e.g. the tmpfile getting the very same
481
- # target FD). The following approach is robust, I believe.
482
- self.targetfd_invalid: int | None = os.open(os.devnull, os.O_RDWR)
483
- os.dup2(self.targetfd_invalid, targetfd)
484
- else:
485
- self.targetfd_invalid = None
486
- self.targetfd_save = os.dup(targetfd)
487
-
488
- if targetfd == 0:
489
- self.tmpfile = open(os.devnull, encoding="utf-8")
490
- self.syscapture: CaptureBase[str] = SysCapture(targetfd)
491
- else:
492
- self.tmpfile = EncodedFile(
493
- TemporaryFile(buffering=0),
494
- encoding="utf-8",
495
- errors="replace",
496
- newline="",
497
- write_through=True,
498
- )
499
- if targetfd in patchsysdict:
500
- self.syscapture = SysCapture(targetfd, self.tmpfile)
501
- else:
502
- self.syscapture = NoCapture(targetfd)
503
-
504
- self._state = "initialized"
505
-
506
- def __repr__(self) -> str:
507
- return (
508
- f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} "
509
- f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
510
- )
511
-
512
- def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
513
- assert self._state in states, (
514
- "cannot {} in state {!r}: expected one of {}".format(
515
- op, self._state, ", ".join(states)
516
- )
517
- )
518
-
519
- def start(self) -> None:
520
- """Start capturing on targetfd using memorized tmpfile."""
521
- self._assert_state("start", ("initialized",))
522
- os.dup2(self.tmpfile.fileno(), self.targetfd)
523
- self.syscapture.start()
524
- self._state = "started"
525
-
526
- def done(self) -> None:
527
- """Stop capturing, restore streams, return original capture file,
528
- seeked to position zero."""
529
- self._assert_state("done", ("initialized", "started", "suspended", "done"))
530
- if self._state == "done":
531
- return
532
- os.dup2(self.targetfd_save, self.targetfd)
533
- os.close(self.targetfd_save)
534
- if self.targetfd_invalid is not None:
535
- if self.targetfd_invalid != self.targetfd:
536
- os.close(self.targetfd)
537
- os.close(self.targetfd_invalid)
538
- self.syscapture.done()
539
- self.tmpfile.close()
540
- self._state = "done"
541
-
542
- def suspend(self) -> None:
543
- self._assert_state("suspend", ("started", "suspended"))
544
- if self._state == "suspended":
545
- return
546
- self.syscapture.suspend()
547
- os.dup2(self.targetfd_save, self.targetfd)
548
- self._state = "suspended"
549
-
550
- def resume(self) -> None:
551
- self._assert_state("resume", ("started", "suspended"))
552
- if self._state == "started":
553
- return
554
- self.syscapture.resume()
555
- os.dup2(self.tmpfile.fileno(), self.targetfd)
556
- self._state = "started"
557
-
558
-
559
- class FDCaptureBinary(FDCaptureBase[bytes]):
560
- """Capture IO to/from a given OS-level file descriptor.
561
-
562
- snap() produces `bytes`.
563
- """
564
-
565
- EMPTY_BUFFER = b""
566
-
567
- def snap(self) -> bytes:
568
- self._assert_state("snap", ("started", "suspended"))
569
- self.tmpfile.seek(0)
570
- res = self.tmpfile.buffer.read()
571
- self.tmpfile.seek(0)
572
- self.tmpfile.truncate()
573
- return res # type: ignore[return-value]
574
-
575
- def writeorg(self, data: bytes) -> None:
576
- """Write to original file descriptor."""
577
- self._assert_state("writeorg", ("started", "suspended"))
578
- os.write(self.targetfd_save, data)
579
-
580
-
581
- class FDCapture(FDCaptureBase[str]):
582
- """Capture IO to/from a given OS-level file descriptor.
583
-
584
- snap() produces text.
585
- """
586
-
587
- EMPTY_BUFFER = ""
588
-
589
- def snap(self) -> str:
590
- self._assert_state("snap", ("started", "suspended"))
591
- self.tmpfile.seek(0)
592
- res = self.tmpfile.read()
593
- self.tmpfile.seek(0)
594
- self.tmpfile.truncate()
595
- return res
596
-
597
- def writeorg(self, data: str) -> None:
598
- """Write to original file descriptor."""
599
- self._assert_state("writeorg", ("started", "suspended"))
600
- # XXX use encoding of original stream
601
- os.write(self.targetfd_save, data.encode("utf-8"))
602
-
603
-
604
- # MultiCapture
605
-
606
-
607
- # Generic NamedTuple only supported since Python 3.11.
608
- if sys.version_info >= (3, 11) or TYPE_CHECKING:
609
-
610
- @final
611
- class CaptureResult(NamedTuple, Generic[AnyStr]):
612
- """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
613
-
614
- out: AnyStr
615
- err: AnyStr
616
-
617
- else:
618
-
619
- class CaptureResult(
620
- collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
621
- Generic[AnyStr],
622
- ):
623
- """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
624
-
625
- __slots__ = ()
626
-
627
-
628
- class MultiCapture(Generic[AnyStr]):
629
- _state = None
630
- _in_suspended = False
631
-
632
- def __init__(
633
- self,
634
- in_: CaptureBase[AnyStr] | None,
635
- out: CaptureBase[AnyStr] | None,
636
- err: CaptureBase[AnyStr] | None,
637
- ) -> None:
638
- self.in_: CaptureBase[AnyStr] | None = in_
639
- self.out: CaptureBase[AnyStr] | None = out
640
- self.err: CaptureBase[AnyStr] | None = err
641
-
642
- def __repr__(self) -> str:
643
- return (
644
- f"<MultiCapture out={self.out!r} err={self.err!r} in_={self.in_!r} "
645
- f"_state={self._state!r} _in_suspended={self._in_suspended!r}>"
646
- )
647
-
648
- def start_capturing(self) -> None:
649
- self._state = "started"
650
- if self.in_:
651
- self.in_.start()
652
- if self.out:
653
- self.out.start()
654
- if self.err:
655
- self.err.start()
656
-
657
- def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]:
658
- """Pop current snapshot out/err capture and flush to orig streams."""
659
- out, err = self.readouterr()
660
- if out:
661
- assert self.out is not None
662
- self.out.writeorg(out)
663
- if err:
664
- assert self.err is not None
665
- self.err.writeorg(err)
666
- return out, err
667
-
668
- def suspend_capturing(self, in_: bool = False) -> None:
669
- self._state = "suspended"
670
- if self.out:
671
- self.out.suspend()
672
- if self.err:
673
- self.err.suspend()
674
- if in_ and self.in_:
675
- self.in_.suspend()
676
- self._in_suspended = True
677
-
678
- def resume_capturing(self) -> None:
679
- self._state = "started"
680
- if self.out:
681
- self.out.resume()
682
- if self.err:
683
- self.err.resume()
684
- if self._in_suspended:
685
- assert self.in_ is not None
686
- self.in_.resume()
687
- self._in_suspended = False
688
-
689
- def stop_capturing(self) -> None:
690
- """Stop capturing and reset capturing streams."""
691
- if self._state == "stopped":
692
- raise ValueError("was already stopped")
693
- self._state = "stopped"
694
- if self.out:
695
- self.out.done()
696
- if self.err:
697
- self.err.done()
698
- if self.in_:
699
- self.in_.done()
700
-
701
- def is_started(self) -> bool:
702
- """Whether actively capturing -- not suspended or stopped."""
703
- return self._state == "started"
704
-
705
- def readouterr(self) -> CaptureResult[AnyStr]:
706
- out = self.out.snap() if self.out else ""
707
- err = self.err.snap() if self.err else ""
708
- # TODO: This type error is real, need to fix.
709
- return CaptureResult(out, err) # type: ignore[arg-type]
710
-
711
-
712
- def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
713
- if method == "fd":
714
- return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
715
- elif method == "sys":
716
- return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2))
717
- elif method == "no":
718
- return MultiCapture(in_=None, out=None, err=None)
719
- elif method == "tee-sys":
720
- return MultiCapture(
721
- in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True)
722
- )
723
- raise ValueError(f"unknown capturing method: {method!r}")
724
-
725
-
726
- # CaptureManager and CaptureFixture
727
-
728
-
729
- class CaptureManager:
730
- """The capture plugin.
731
-
732
- Manages that the appropriate capture method is enabled/disabled during
733
- collection and each test phase (setup, call, teardown). After each of
734
- those points, the captured output is obtained and attached to the
735
- collection/runtest report.
736
-
737
- There are two levels of capture:
738
-
739
- * global: enabled by default and can be suppressed by the ``-s``
740
- option. This is always enabled/disabled during collection and each test
741
- phase.
742
-
743
- * fixture: when a test function or one of its fixture depend on the
744
- ``capsys`` or ``capfd`` fixtures. In this case special handling is
745
- needed to ensure the fixtures take precedence over the global capture.
746
- """
747
-
748
- def __init__(self, method: _CaptureMethod) -> None:
749
- self._method: Final = method
750
- self._global_capturing: MultiCapture[str] | None = None
751
- self._capture_fixture: CaptureFixture[Any] | None = None
752
-
753
- def __repr__(self) -> str:
754
- return (
755
- f"<CaptureManager _method={self._method!r} _global_capturing={self._global_capturing!r} "
756
- f"_capture_fixture={self._capture_fixture!r}>"
757
- )
758
-
759
- def is_capturing(self) -> str | bool:
760
- if self.is_globally_capturing():
761
- return "global"
762
- if self._capture_fixture:
763
- return f"fixture {self._capture_fixture.request.fixturename}"
764
- return False
765
-
766
- # Global capturing control
767
-
768
- def is_globally_capturing(self) -> bool:
769
- return self._method != "no"
770
-
771
- def start_global_capturing(self) -> None:
772
- assert self._global_capturing is None
773
- self._global_capturing = _get_multicapture(self._method)
774
- self._global_capturing.start_capturing()
775
-
776
- def stop_global_capturing(self) -> None:
777
- if self._global_capturing is not None:
778
- self._global_capturing.pop_outerr_to_orig()
779
- self._global_capturing.stop_capturing()
780
- self._global_capturing = None
781
-
782
- def resume_global_capture(self) -> None:
783
- # During teardown of the python process, and on rare occasions, capture
784
- # attributes can be `None` while trying to resume global capture.
785
- if self._global_capturing is not None:
786
- self._global_capturing.resume_capturing()
787
-
788
- def suspend_global_capture(self, in_: bool = False) -> None:
789
- if self._global_capturing is not None:
790
- self._global_capturing.suspend_capturing(in_=in_)
791
-
792
- def suspend(self, in_: bool = False) -> None:
793
- # Need to undo local capsys-et-al if it exists before disabling global capture.
794
- self.suspend_fixture()
795
- self.suspend_global_capture(in_)
796
-
797
- def resume(self) -> None:
798
- self.resume_global_capture()
799
- self.resume_fixture()
800
-
801
- def read_global_capture(self) -> CaptureResult[str]:
802
- assert self._global_capturing is not None
803
- return self._global_capturing.readouterr()
804
-
805
- # Fixture Control
806
-
807
- def set_fixture(self, capture_fixture: CaptureFixture[Any]) -> None:
808
- if self._capture_fixture:
809
- current_fixture = self._capture_fixture.request.fixturename
810
- requested_fixture = capture_fixture.request.fixturename
811
- capture_fixture.request.raiseerror(
812
- f"cannot use {requested_fixture} and {current_fixture} at the same time"
813
- )
814
- self._capture_fixture = capture_fixture
815
-
816
- def unset_fixture(self) -> None:
817
- self._capture_fixture = None
818
-
819
- def activate_fixture(self) -> None:
820
- """If the current item is using ``capsys`` or ``capfd``, activate
821
- them so they take precedence over the global capture."""
822
- if self._capture_fixture:
823
- self._capture_fixture._start()
824
-
825
- def deactivate_fixture(self) -> None:
826
- """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""
827
- if self._capture_fixture:
828
- self._capture_fixture.close()
829
-
830
- def suspend_fixture(self) -> None:
831
- if self._capture_fixture:
832
- self._capture_fixture._suspend()
833
-
834
- def resume_fixture(self) -> None:
835
- if self._capture_fixture:
836
- self._capture_fixture._resume()
837
-
838
- # Helper context managers
839
-
840
- @contextlib.contextmanager
841
- def global_and_fixture_disabled(self) -> Generator[None]:
842
- """Context manager to temporarily disable global and current fixture capturing."""
843
- do_fixture = self._capture_fixture and self._capture_fixture._is_started()
844
- if do_fixture:
845
- self.suspend_fixture()
846
- do_global = self._global_capturing and self._global_capturing.is_started()
847
- if do_global:
848
- self.suspend_global_capture()
849
- try:
850
- yield
851
- finally:
852
- if do_global:
853
- self.resume_global_capture()
854
- if do_fixture:
855
- self.resume_fixture()
856
-
857
- @contextlib.contextmanager
858
- def item_capture(self, when: str, item: Item) -> Generator[None]:
859
- self.resume_global_capture()
860
- self.activate_fixture()
861
- try:
862
- yield
863
- finally:
864
- self.deactivate_fixture()
865
- self.suspend_global_capture(in_=False)
866
-
867
- out, err = self.read_global_capture()
868
- item.add_report_section(when, "stdout", out)
869
- item.add_report_section(when, "stderr", err)
870
-
871
- # Hooks
872
-
873
- @hookimpl(wrapper=True)
874
- def pytest_make_collect_report(
875
- self, collector: Collector
876
- ) -> Generator[None, CollectReport, CollectReport]:
877
- if isinstance(collector, File):
878
- self.resume_global_capture()
879
- try:
880
- rep = yield
881
- finally:
882
- self.suspend_global_capture()
883
- out, err = self.read_global_capture()
884
- if out:
885
- rep.sections.append(("Captured stdout", out))
886
- if err:
887
- rep.sections.append(("Captured stderr", err))
888
- else:
889
- rep = yield
890
- return rep
891
-
892
- @hookimpl(wrapper=True)
893
- def pytest_runtest_setup(self, item: Item) -> Generator[None]:
894
- with self.item_capture("setup", item):
895
- return (yield)
896
-
897
- @hookimpl(wrapper=True)
898
- def pytest_runtest_call(self, item: Item) -> Generator[None]:
899
- with self.item_capture("call", item):
900
- return (yield)
901
-
902
- @hookimpl(wrapper=True)
903
- def pytest_runtest_teardown(self, item: Item) -> Generator[None]:
904
- with self.item_capture("teardown", item):
905
- return (yield)
906
-
907
- @hookimpl(tryfirst=True)
908
- def pytest_keyboard_interrupt(self) -> None:
909
- self.stop_global_capturing()
910
-
911
- @hookimpl(tryfirst=True)
912
- def pytest_internalerror(self) -> None:
913
- self.stop_global_capturing()
914
-
915
-
916
- class CaptureFixture(Generic[AnyStr]):
917
- """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,
918
- :fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
919
-
920
- def __init__(
921
- self,
922
- captureclass: type[CaptureBase[AnyStr]],
923
- request: SubRequest,
924
- *,
925
- config: dict[str, Any] | None = None,
926
- _ispytest: bool = False,
927
- ) -> None:
928
- check_ispytest(_ispytest)
929
- self.captureclass: type[CaptureBase[AnyStr]] = captureclass
930
- self.request = request
931
- self._config = config if config else {}
932
- self._capture: MultiCapture[AnyStr] | None = None
933
- self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
934
- self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
935
-
936
- def _start(self) -> None:
937
- if self._capture is None:
938
- self._capture = MultiCapture(
939
- in_=None,
940
- out=self.captureclass(1, **self._config),
941
- err=self.captureclass(2, **self._config),
942
- )
943
- self._capture.start_capturing()
944
-
945
- def close(self) -> None:
946
- if self._capture is not None:
947
- if self._config.get("tee"):
948
- # When tee is enabled, output was already written to the
949
- # original stream in real-time by TeeCaptureIO. Using
950
- # pop_outerr_to_orig() would write it a second time via
951
- # writeorg(), causing doubled output (see #13784).
952
- out, err = self._capture.readouterr()
953
- else:
954
- out, err = self._capture.pop_outerr_to_orig()
955
- self._captured_out += out
956
- self._captured_err += err
957
- self._capture.stop_capturing()
958
- self._capture = None
959
-
960
- def readouterr(self) -> CaptureResult[AnyStr]:
961
- """Read and return the captured output so far, resetting the internal
962
- buffer.
963
-
964
- :returns:
965
- The captured content as a namedtuple with ``out`` and ``err``
966
- string attributes.
967
- """
968
- captured_out, captured_err = self._captured_out, self._captured_err
969
- if self._capture is not None:
970
- out, err = self._capture.readouterr()
971
- captured_out += out
972
- captured_err += err
973
- self._captured_out = self.captureclass.EMPTY_BUFFER
974
- self._captured_err = self.captureclass.EMPTY_BUFFER
975
- return CaptureResult(captured_out, captured_err)
976
-
977
- def _suspend(self) -> None:
978
- """Suspend this fixture's own capturing temporarily."""
979
- if self._capture is not None:
980
- self._capture.suspend_capturing()
981
-
982
- def _resume(self) -> None:
983
- """Resume this fixture's own capturing temporarily."""
984
- if self._capture is not None:
985
- self._capture.resume_capturing()
986
-
987
- def _is_started(self) -> bool:
988
- """Whether actively capturing -- not disabled or closed."""
989
- if self._capture is not None:
990
- return self._capture.is_started()
991
- return False
992
-
993
- @contextlib.contextmanager
994
- def disabled(self) -> Generator[None]:
995
- """Temporarily disable capturing while inside the ``with`` block."""
996
- capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
997
- "capturemanager"
998
- )
999
- with capmanager.global_and_fixture_disabled():
1000
- yield
1001
-
1002
-
1003
- # The fixtures.
1004
-
1005
-
1006
- @fixture
1007
- def capsys(request: SubRequest) -> Generator[CaptureFixture[str]]:
1008
- r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
1009
-
1010
- The captured output is made available via ``capsys.readouterr()`` method
1011
- calls, which return a ``(out, err)`` namedtuple.
1012
- ``out`` and ``err`` will be ``text`` objects.
1013
-
1014
- Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
1015
-
1016
- Example:
1017
-
1018
- .. code-block:: python
1019
-
1020
- def test_output(capsys):
1021
- print("hello")
1022
- captured = capsys.readouterr()
1023
- assert captured.out == "hello\n"
1024
- """
1025
- capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1026
- capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
1027
- capman.set_fixture(capture_fixture)
1028
- capture_fixture._start()
1029
- yield capture_fixture
1030
- capture_fixture.close()
1031
- capman.unset_fixture()
1032
-
1033
-
1034
- @fixture
1035
- def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]:
1036
- r"""Enable simultaneous text capturing and pass-through of writes
1037
- to ``sys.stdout`` and ``sys.stderr`` as defined by ``--capture=``.
1038
-
1039
-
1040
- The captured output is made available via ``capteesys.readouterr()`` method
1041
- calls, which return a ``(out, err)`` namedtuple.
1042
- ``out`` and ``err`` will be ``text`` objects.
1043
-
1044
- The output is also passed-through, allowing it to be "live-printed",
1045
- reported, or both as defined by ``--capture=``.
1046
-
1047
- Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
1048
-
1049
- Example:
1050
-
1051
- .. code-block:: python
1052
-
1053
- def test_output(capteesys):
1054
- print("hello")
1055
- captured = capteesys.readouterr()
1056
- assert captured.out == "hello\n"
1057
- """
1058
- capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1059
- capture_fixture = CaptureFixture(
1060
- SysCapture, request, config=dict(tee=True), _ispytest=True
1061
- )
1062
- capman.set_fixture(capture_fixture)
1063
- capture_fixture._start()
1064
- yield capture_fixture
1065
- capture_fixture.close()
1066
- capman.unset_fixture()
1067
-
1068
-
1069
- @fixture
1070
- def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
1071
- r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
1072
-
1073
- The captured output is made available via ``capsysbinary.readouterr()``
1074
- method calls, which return a ``(out, err)`` namedtuple.
1075
- ``out`` and ``err`` will be ``bytes`` objects.
1076
-
1077
- Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
1078
-
1079
- Example:
1080
-
1081
- .. code-block:: python
1082
-
1083
- def test_output(capsysbinary):
1084
- print("hello")
1085
- captured = capsysbinary.readouterr()
1086
- assert captured.out == b"hello\n"
1087
- """
1088
- capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1089
- capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
1090
- capman.set_fixture(capture_fixture)
1091
- capture_fixture._start()
1092
- yield capture_fixture
1093
- capture_fixture.close()
1094
- capman.unset_fixture()
1095
-
1096
-
1097
- @fixture
1098
- def capfd(request: SubRequest) -> Generator[CaptureFixture[str]]:
1099
- r"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
1100
-
1101
- The captured output is made available via ``capfd.readouterr()`` method
1102
- calls, which return a ``(out, err)`` namedtuple.
1103
- ``out`` and ``err`` will be ``text`` objects.
1104
-
1105
- Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
1106
-
1107
- Example:
1108
-
1109
- .. code-block:: python
1110
-
1111
- def test_system_echo(capfd):
1112
- os.system('echo "hello"')
1113
- captured = capfd.readouterr()
1114
- assert captured.out == "hello\n"
1115
- """
1116
- capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1117
- capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
1118
- capman.set_fixture(capture_fixture)
1119
- capture_fixture._start()
1120
- yield capture_fixture
1121
- capture_fixture.close()
1122
- capman.unset_fixture()
1123
-
1124
-
1125
- @fixture
1126
- def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
1127
- r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
1128
-
1129
- The captured output is made available via ``capfd.readouterr()`` method
1130
- calls, which return a ``(out, err)`` namedtuple.
1131
- ``out`` and ``err`` will be ``byte`` objects.
1132
-
1133
- Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
1134
-
1135
- Example:
1136
-
1137
- .. code-block:: python
1138
-
1139
- def test_system_echo(capfdbinary):
1140
- os.system('echo "hello"')
1141
- captured = capfdbinary.readouterr()
1142
- assert captured.out == b"hello\n"
1143
-
1144
- """
1145
- capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
1146
- capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
1147
- capman.set_fixture(capture_fixture)
1148
- capture_fixture._start()
1149
- yield capture_fixture
1150
- capture_fixture.close()
1151
- capman.unset_fixture()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/compat.py DELETED
@@ -1,329 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- """Python version compatibility code and random general utilities."""
3
-
4
- from __future__ import annotations
5
-
6
- from collections.abc import Callable
7
- import enum
8
- import functools
9
- import inspect
10
- from inspect import Parameter
11
- from inspect import Signature
12
- import os
13
- from pathlib import Path
14
- import sys
15
- from typing import Any
16
- from typing import Final
17
- from typing import NoReturn
18
- from typing import TYPE_CHECKING
19
-
20
- import py
21
-
22
-
23
- if sys.version_info >= (3, 14):
24
- from annotationlib import Format
25
-
26
-
27
- #: constant to prepare valuing pylib path replacements/lazy proxies later on
28
- # intended for removal in pytest 8.0 or 9.0
29
-
30
- # fmt: off
31
- # intentional space to create a fake difference for the verification
32
- LEGACY_PATH = py.path. local
33
- # fmt: on
34
-
35
-
36
- def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
37
- """Internal wrapper to prepare lazy proxies for legacy_path instances"""
38
- return LEGACY_PATH(path)
39
-
40
-
41
- # fmt: off
42
- # Singleton type for NOTSET, as described in:
43
- # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
44
- class NotSetType(enum.Enum):
45
- token = 0
46
- NOTSET: Final = NotSetType.token
47
- # fmt: on
48
-
49
-
50
- def iscoroutinefunction(func: object) -> bool:
51
- """Return True if func is a coroutine function (a function defined with async
52
- def syntax, and doesn't contain yield), or a function decorated with
53
- @asyncio.coroutine.
54
-
55
- Note: copied and modified from Python 3.5's builtin coroutines.py to avoid
56
- importing asyncio directly, which in turns also initializes the "logging"
57
- module as a side-effect (see issue #8).
58
- """
59
- return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
60
-
61
-
62
- def is_async_function(func: object) -> bool:
63
- """Return True if the given function seems to be an async function or
64
- an async generator."""
65
- return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
66
-
67
-
68
- def signature(obj: Callable[..., Any]) -> Signature:
69
- """Return signature without evaluating annotations."""
70
- if sys.version_info >= (3, 14):
71
- return inspect.signature(obj, annotation_format=Format.STRING)
72
- return inspect.signature(obj)
73
-
74
-
75
- def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
76
- function = get_real_func(function)
77
- fn = Path(inspect.getfile(function))
78
- lineno = function.__code__.co_firstlineno
79
- if curdir is not None:
80
- try:
81
- relfn = fn.relative_to(curdir)
82
- except ValueError:
83
- pass
84
- else:
85
- return f"{relfn}:{lineno + 1}"
86
- return f"{fn}:{lineno + 1}"
87
-
88
-
89
- def num_mock_patch_args(function) -> int:
90
- """Return number of arguments used up by mock arguments (if any)."""
91
- patchings = getattr(function, "patchings", None)
92
- if not patchings:
93
- return 0
94
-
95
- mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object())
96
- ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object())
97
-
98
- return len(
99
- [
100
- p
101
- for p in patchings
102
- if not p.attribute_name
103
- and (p.new is mock_sentinel or p.new is ut_mock_sentinel)
104
- ]
105
- )
106
-
107
-
108
- def getfuncargnames(
109
- function: Callable[..., object],
110
- *,
111
- name: str = "",
112
- cls: type | None = None,
113
- ) -> tuple[str, ...]:
114
- """Return the names of a function's mandatory arguments.
115
-
116
- Should return the names of all function arguments that:
117
- * Aren't bound to an instance or type as in instance or class methods.
118
- * Don't have default values.
119
- * Aren't bound with functools.partial.
120
- * Aren't replaced with mocks.
121
-
122
- The cls arguments indicate that the function should be treated as a bound
123
- method even though it's not unless the function is a static method.
124
-
125
- The name parameter should be the original name in which the function was collected.
126
- """
127
- # TODO(RonnyPfannschmidt): This function should be refactored when we
128
- # revisit fixtures. The fixture mechanism should ask the node for
129
- # the fixture names, and not try to obtain directly from the
130
- # function object well after collection has occurred.
131
-
132
- # The parameters attribute of a Signature object contains an
133
- # ordered mapping of parameter names to Parameter instances. This
134
- # creates a tuple of the names of the parameters that don't have
135
- # defaults.
136
- try:
137
- parameters = signature(function).parameters.values()
138
- except (ValueError, TypeError) as e:
139
- from _pytest.outcomes import fail
140
-
141
- fail(
142
- f"Could not determine arguments of {function!r}: {e}",
143
- pytrace=False,
144
- )
145
-
146
- arg_names = tuple(
147
- p.name
148
- for p in parameters
149
- if (
150
- p.kind is Parameter.POSITIONAL_OR_KEYWORD
151
- or p.kind is Parameter.KEYWORD_ONLY
152
- )
153
- and p.default is Parameter.empty
154
- )
155
- if not name:
156
- name = function.__name__
157
-
158
- # If this function should be treated as a bound method even though
159
- # it's passed as an unbound method or function, and its first parameter
160
- # wasn't defined as positional only, remove the first parameter name.
161
- if not any(p.kind is Parameter.POSITIONAL_ONLY for p in parameters) and (
162
- # Not using `getattr` because we don't want to resolve the staticmethod.
163
- # Not using `cls.__dict__` because we want to check the entire MRO.
164
- cls
165
- and not isinstance(
166
- inspect.getattr_static(cls, name, default=None), staticmethod
167
- )
168
- ):
169
- arg_names = arg_names[1:]
170
- # Remove any names that will be replaced with mocks.
171
- if hasattr(function, "__wrapped__"):
172
- arg_names = arg_names[num_mock_patch_args(function) :]
173
- return arg_names
174
-
175
-
176
- def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
177
- # Note: this code intentionally mirrors the code at the beginning of
178
- # getfuncargnames, to get the arguments which were excluded from its result
179
- # because they had default values.
180
- return tuple(
181
- p.name
182
- for p in signature(function).parameters.values()
183
- if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
184
- and p.default is not Parameter.empty
185
- )
186
-
187
-
188
- _non_printable_ascii_translate_table = {
189
- i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127)
190
- }
191
- _non_printable_ascii_translate_table.update(
192
- {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"}
193
- )
194
-
195
-
196
- def ascii_escaped(val: bytes | str) -> str:
197
- r"""If val is pure ASCII, return it as an str, otherwise, escape
198
- bytes objects into a sequence of escaped bytes:
199
-
200
- b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
201
-
202
- and escapes strings into a sequence of escaped unicode ids, e.g.:
203
-
204
- r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
205
-
206
- Note:
207
- The obvious "v.decode('unicode-escape')" will return
208
- valid UTF-8 unicode if it finds them in bytes, but we
209
- want to return escaped bytes for any byte, even if they match
210
- a UTF-8 string.
211
- """
212
- if isinstance(val, bytes):
213
- ret = val.decode("ascii", "backslashreplace")
214
- else:
215
- ret = val.encode("unicode_escape").decode("ascii")
216
- return ret.translate(_non_printable_ascii_translate_table)
217
-
218
-
219
- def get_real_func(obj):
220
- """Get the real function object of the (possibly) wrapped object by
221
- :func:`functools.wraps`, or :func:`functools.partial`."""
222
- obj = inspect.unwrap(obj)
223
-
224
- if isinstance(obj, functools.partial):
225
- obj = obj.func
226
- return obj
227
-
228
-
229
- def getimfunc(func):
230
- try:
231
- return func.__func__
232
- except AttributeError:
233
- return func
234
-
235
-
236
- def safe_getattr(object: Any, name: str, default: Any) -> Any:
237
- """Like getattr but return default upon any Exception or any OutcomeException.
238
-
239
- Attribute access can potentially fail for 'evil' Python objects.
240
- See issue #214.
241
- It catches OutcomeException because of #2490 (issue #580), new outcomes
242
- are derived from BaseException instead of Exception (for more details
243
- check #2707).
244
- """
245
- from _pytest.outcomes import TEST_OUTCOME
246
-
247
- try:
248
- return getattr(object, name, default)
249
- except TEST_OUTCOME:
250
- return default
251
-
252
-
253
- def safe_isclass(obj: object) -> bool:
254
- """Ignore any exception via isinstance on Python 3."""
255
- try:
256
- return inspect.isclass(obj)
257
- except Exception:
258
- return False
259
-
260
-
261
- def get_user_id() -> int | None:
262
- """Return the current process's real user id or None if it could not be
263
- determined.
264
-
265
- :return: The user id or None if it could not be determined.
266
- """
267
- # mypy follows the version and platform checking expectation of PEP 484:
268
- # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
269
- # Containment checks are too complex for mypy v1.5.0 and cause failure.
270
- if sys.platform == "win32" or sys.platform == "emscripten":
271
- # win32 does not have a getuid() function.
272
- # Emscripten has a return 0 stub.
273
- return None
274
- else:
275
- # On other platforms, a return value of -1 is assumed to indicate that
276
- # the current process's real user id could not be determined.
277
- ERROR = -1
278
- uid = os.getuid()
279
- return uid if uid != ERROR else None
280
-
281
-
282
- if sys.version_info >= (3, 11):
283
- from typing import assert_never
284
- else:
285
-
286
- def assert_never(value: NoReturn) -> NoReturn:
287
- assert False, f"Unhandled value: {value} ({type(value).__name__})"
288
-
289
-
290
- class CallableBool:
291
- """
292
- A bool-like object that can also be called, returning its true/false value.
293
-
294
- Used for backwards compatibility in cases where something was supposed to be a method
295
- but was implemented as a simple attribute by mistake (see `TerminalReporter.isatty`).
296
-
297
- Do not use in new code.
298
- """
299
-
300
- def __init__(self, value: bool) -> None:
301
- self._value = value
302
-
303
- def __bool__(self) -> bool:
304
- return self._value
305
-
306
- def __call__(self) -> bool:
307
- return self._value
308
-
309
-
310
- def running_on_ci() -> bool:
311
- """Check if we're currently running on a CI system."""
312
- # Only enable CI mode if one of these env variables is defined and non-empty.
313
- # Note: review `regendoc` tox env in case this list is changed.
314
- env_vars = ["CI", "BUILD_NUMBER"]
315
- return any(os.environ.get(var) for var in env_vars)
316
-
317
-
318
- if sys.version_info >= (3, 13):
319
- from warnings import deprecated as deprecated
320
- else:
321
- if TYPE_CHECKING:
322
- from typing_extensions import deprecated as deprecated
323
- else:
324
-
325
- def deprecated(msg, /, *, category=None, stacklevel=1):
326
- def decorator(func):
327
- return func
328
-
329
- return decorator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/config/__init__.py DELETED
@@ -1,2246 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- """Command line options, config-file and conftest.py processing."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import builtins
8
- import collections.abc
9
- from collections.abc import Callable
10
- from collections.abc import Generator
11
- from collections.abc import Iterable
12
- from collections.abc import Iterator
13
- from collections.abc import Mapping
14
- from collections.abc import MutableMapping
15
- from collections.abc import Sequence
16
- import contextlib
17
- import copy
18
- import dataclasses
19
- import enum
20
- from functools import lru_cache
21
- import glob
22
- import importlib
23
- import importlib.metadata
24
- import inspect
25
- import os
26
- import pathlib
27
- import re
28
- import shlex
29
- import sys
30
- from textwrap import dedent
31
- import types
32
- from types import FunctionType
33
- from typing import Any
34
- from typing import cast
35
- from typing import Final
36
- from typing import final
37
- from typing import IO
38
- from typing import TextIO
39
- from typing import TYPE_CHECKING
40
- import warnings
41
-
42
- from pluggy import HookimplMarker
43
- from pluggy import HookimplOpts
44
- from pluggy import HookspecMarker
45
- from pluggy import HookspecOpts
46
- from pluggy import PluginManager
47
-
48
- from .exceptions import PrintHelp as PrintHelp
49
- from .exceptions import UsageError as UsageError
50
- from .findpaths import ConfigDict
51
- from .findpaths import ConfigValue
52
- from .findpaths import determine_setup
53
- from .findpaths import parse_override_ini
54
- from _pytest import __version__
55
- import _pytest._code
56
- from _pytest._code import ExceptionInfo
57
- from _pytest._code import filter_traceback
58
- from _pytest._code.code import TracebackStyle
59
- from _pytest._io import TerminalWriter
60
- from _pytest.compat import assert_never
61
- from _pytest.compat import deprecated
62
- from _pytest.compat import NOTSET
63
- from _pytest.config.argparsing import Argument
64
- from _pytest.config.argparsing import FILE_OR_DIR
65
- from _pytest.config.argparsing import Parser
66
- import _pytest.deprecated
67
- import _pytest.hookspec
68
- from _pytest.outcomes import fail
69
- from _pytest.outcomes import Skipped
70
- from _pytest.pathlib import absolutepath
71
- from _pytest.pathlib import bestrelpath
72
- from _pytest.pathlib import import_path
73
- from _pytest.pathlib import ImportMode
74
- from _pytest.pathlib import resolve_package_path
75
- from _pytest.pathlib import safe_exists
76
- from _pytest.stash import Stash
77
- from _pytest.warning_types import PytestConfigWarning
78
- from _pytest.warning_types import warn_explicit_for
79
-
80
-
81
- if TYPE_CHECKING:
82
- from _pytest.assertion.rewrite import AssertionRewritingHook
83
- from _pytest.cacheprovider import Cache
84
- from _pytest.terminal import TerminalReporter
85
-
86
- _PluggyPlugin = object
87
- """A type to represent plugin objects.
88
-
89
- Plugins can be any namespace, so we can't narrow it down much, but we use an
90
- alias to make the intent clear.
91
-
92
- Ideally this type would be provided by pluggy itself.
93
- """
94
-
95
-
96
- hookimpl = HookimplMarker("pytest")
97
- hookspec = HookspecMarker("pytest")
98
-
99
-
100
- @final
101
- class ExitCode(enum.IntEnum):
102
- """Encodes the valid exit codes by pytest.
103
-
104
- Currently users and plugins may supply other exit codes as well.
105
-
106
- .. versionadded:: 5.0
107
- """
108
-
109
- #: Tests passed.
110
- OK = 0
111
- #: Tests failed.
112
- TESTS_FAILED = 1
113
- #: pytest was interrupted.
114
- INTERRUPTED = 2
115
- #: An internal error got in the way.
116
- INTERNAL_ERROR = 3
117
- #: pytest was misused.
118
- USAGE_ERROR = 4
119
- #: pytest couldn't find tests.
120
- NO_TESTS_COLLECTED = 5
121
- #: All tests pass, but maximum number of warnings exceeded.
122
- MAX_WARNINGS_ERROR = 6
123
-
124
- __module__ = "pytest"
125
-
126
-
127
- class ConftestImportFailure(Exception):
128
- def __init__(
129
- self,
130
- path: pathlib.Path,
131
- *,
132
- cause: Exception,
133
- ) -> None:
134
- self.path = path
135
- self.cause = cause
136
-
137
- def __str__(self) -> str:
138
- return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
139
-
140
-
141
- def filter_traceback_for_conftest_import_failure(
142
- entry: _pytest._code.TracebackEntry,
143
- ) -> bool:
144
- """Filter tracebacks entries which point to pytest internals or importlib.
145
-
146
- Make a special case for importlib because we use it to import test modules and conftest files
147
- in _pytest.pathlib.import_path.
148
- """
149
- return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)
150
-
151
-
152
- def print_conftest_import_error(e: ConftestImportFailure, file: TextIO) -> None:
153
- exc_info = ExceptionInfo.from_exception(e.cause)
154
- tw = TerminalWriter(file)
155
- tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
156
- exc_info.traceback = exc_info.traceback.filter(
157
- filter_traceback_for_conftest_import_failure
158
- )
159
- exc_repr = (
160
- exc_info.getrepr(style="short", chain=False)
161
- if exc_info.traceback
162
- else exc_info.exconly()
163
- )
164
- formatted_tb = str(exc_repr)
165
- for line in formatted_tb.splitlines():
166
- tw.line(line.rstrip(), red=True)
167
-
168
-
169
- def print_usage_error(e: UsageError, file: TextIO) -> None:
170
- tw = TerminalWriter(file)
171
- for msg in e.args:
172
- tw.line(f"ERROR: {msg}\n", red=True)
173
-
174
-
175
- def _get_prog_name(argv: Sequence[str]) -> str:
176
- """Determine the CLI program name from the argument vector.
177
-
178
- :param argv: The argument vector (typically ``sys.argv``).
179
- :returns: ``"python -m pytest"`` when invoked via ``python -m``,
180
- ``"pytest"`` otherwise.
181
- """
182
- argv0 = argv[0] if argv else ""
183
- if os.path.basename(argv0) == "__main__.py":
184
- return "python -m pytest"
185
- return "pytest"
186
-
187
-
188
- def main(
189
- args: list[str] | os.PathLike[str] | None = None,
190
- plugins: Sequence[str | _PluggyPlugin] | None = None,
191
- ) -> int | ExitCode:
192
- """Perform an in-process test run.
193
-
194
- :param args:
195
- List of command line arguments. If `None` or not given, defaults to reading
196
- arguments directly from the process command line (:data:`sys.argv`).
197
- :param plugins: List of plugin objects to be auto-registered during initialization.
198
-
199
- :returns: An exit code.
200
- """
201
- return _main(args=args, plugins=plugins, prog="pytest.main()")
202
-
203
-
204
- def _main(
205
- *,
206
- args: list[str] | os.PathLike[str] | None = None,
207
- plugins: Sequence[str | _PluggyPlugin] | None = None,
208
- prog: str,
209
- ) -> int | ExitCode:
210
- # Handle a single `--version`/`-V` argument early to avoid starting up the entire pytest infrastructure.
211
- new_args = sys.argv[1:] if args is None else args
212
- if (
213
- isinstance(new_args, Sequence)
214
- and (new_args.count("--version") + new_args.count("-V")) == 1
215
- ):
216
- sys.stdout.write(f"pytest {__version__}\n")
217
- return ExitCode.OK
218
-
219
- old_pytest_version = os.environ.get("PYTEST_VERSION")
220
- try:
221
- os.environ["PYTEST_VERSION"] = __version__
222
- try:
223
- config = _prepareconfig(new_args, plugins, prog=prog)
224
- except ConftestImportFailure as e:
225
- print_conftest_import_error(e, file=sys.stderr)
226
- return ExitCode.USAGE_ERROR
227
-
228
- try:
229
- ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
230
- try:
231
- return ExitCode(ret)
232
- except ValueError:
233
- return ret
234
- finally:
235
- config._ensure_unconfigure()
236
- except UsageError as e:
237
- print_usage_error(e, file=sys.stderr)
238
- return ExitCode.USAGE_ERROR
239
- finally:
240
- if old_pytest_version is None:
241
- os.environ.pop("PYTEST_VERSION", None)
242
- else:
243
- os.environ["PYTEST_VERSION"] = old_pytest_version
244
-
245
-
246
- def _console_main() -> int:
247
- """The CLI entry point of pytest (internal).
248
-
249
- This is the real implementation used by entry points and ``__main__.py``.
250
- """
251
- # https://docs.python.org/3/library/signal.html#note-on-sigpipe
252
- try:
253
- code = _main(prog=_get_prog_name(sys.argv))
254
- sys.stdout.flush()
255
- return code
256
- except BrokenPipeError:
257
- # Python flushes standard streams on exit; redirect remaining output
258
- # to devnull to avoid another BrokenPipeError at shutdown
259
- devnull = os.open(os.devnull, os.O_WRONLY)
260
- os.dup2(devnull, sys.stdout.fileno())
261
- return 1 # Python exits with error code 1 on EPIPE
262
-
263
-
264
- def console_main() -> int:
265
- """The CLI entry point of pytest.
266
-
267
- .. deprecated:: 9.1
268
- This function is slated for removal in pytest 10.
269
- It is not meant for programmable use; use :func:`pytest.main` instead.
270
- """
271
- import warnings
272
-
273
- from _pytest.deprecated import CONSOLE_MAIN
274
-
275
- warnings.warn(CONSOLE_MAIN, stacklevel=2)
276
- return _console_main()
277
-
278
-
279
- class cmdline: # compatibility namespace
280
- main = staticmethod(main)
281
-
282
-
283
- def filename_arg(path: str, optname: str) -> str:
284
- """Argparse type validator for filename arguments.
285
-
286
- :path: Path of filename.
287
- :optname: Name of the option.
288
- """
289
- if os.path.isdir(path):
290
- raise UsageError(f"{optname} must be a filename, given: {path}")
291
- return path
292
-
293
-
294
- def directory_arg(path: str, optname: str) -> str:
295
- """Argparse type validator for directory arguments.
296
-
297
- :path: Path of directory.
298
- :optname: Name of the option.
299
- """
300
- if not os.path.isdir(path):
301
- raise UsageError(f"{optname} must be a directory, given: {path}")
302
- return path
303
-
304
-
305
- # Plugins that cannot be disabled via "-p no:X" currently.
306
- essential_plugins = (
307
- "mark",
308
- "main",
309
- "runner",
310
- "fixtures",
311
- "helpconfig", # Provides -p.
312
- )
313
-
314
- default_plugins = (
315
- *essential_plugins,
316
- "python",
317
- "terminal",
318
- "debugging",
319
- "unittest",
320
- "capture",
321
- "skipping",
322
- "legacypath",
323
- "tmpdir",
324
- "monkeypatch",
325
- "recwarn",
326
- "pastebin",
327
- "assertion",
328
- "junitxml",
329
- "doctest",
330
- "cacheprovider",
331
- "setuponly",
332
- "setupplan",
333
- "stepwise",
334
- "unraisableexception",
335
- "threadexception",
336
- "warnings",
337
- "logging",
338
- "reports",
339
- "faulthandler",
340
- "subtests",
341
- )
342
-
343
- builtin_plugins = {
344
- *default_plugins,
345
- "pytester",
346
- "pytester_assertions",
347
- "terminalprogress",
348
- }
349
-
350
-
351
- def get_config(
352
- args: Iterable[str] | None = None,
353
- plugins: Sequence[str | _PluggyPlugin] | None = None,
354
- *,
355
- prog: str | None = None,
356
- ) -> Config:
357
- # Subsequent calls to main will create a fresh instance.
358
- pluginmanager = PytestPluginManager()
359
- invocation_params = Config.InvocationParams(
360
- args=args or (),
361
- plugins=plugins,
362
- dir=pathlib.Path.cwd(),
363
- )
364
- config = Config(pluginmanager, invocation_params=invocation_params, prog=prog)
365
-
366
- if invocation_params.args:
367
- # Handle any "-p no:plugin" args.
368
- pluginmanager.consider_preparse(invocation_params.args, exclude_only=True)
369
-
370
- for spec in default_plugins:
371
- pluginmanager.import_plugin(spec)
372
-
373
- return config
374
-
375
-
376
- def get_plugin_manager() -> PytestPluginManager:
377
- """Obtain a new instance of the
378
- :py:class:`pytest.PytestPluginManager`, with default plugins
379
- already loaded.
380
-
381
- This function can be used by integration with other tools, like hooking
382
- into pytest to run tests into an IDE.
383
- """
384
- return get_config().pluginmanager
385
-
386
-
387
- def _prepareconfig(
388
- args: list[str] | os.PathLike[str],
389
- plugins: Sequence[str | _PluggyPlugin] | None = None,
390
- *,
391
- prog: str | None = None,
392
- ) -> Config:
393
- if isinstance(args, os.PathLike):
394
- args = [os.fspath(args)]
395
- elif not isinstance(args, list):
396
- msg = ( # type:ignore[unreachable]
397
- "`args` parameter expected to be a list of strings, got: {!r} (type: {})"
398
- )
399
- raise TypeError(msg.format(args, type(args)))
400
-
401
- initial_config = get_config(args, plugins, prog=prog)
402
- pluginmanager = initial_config.pluginmanager
403
- try:
404
- if plugins:
405
- for plugin in plugins:
406
- if isinstance(plugin, str):
407
- pluginmanager.consider_pluginarg(plugin)
408
- else:
409
- pluginmanager.register(plugin)
410
- config: Config = pluginmanager.hook.pytest_cmdline_parse(
411
- pluginmanager=pluginmanager, args=args
412
- )
413
- return config
414
- except BaseException:
415
- initial_config._ensure_unconfigure()
416
- raise
417
-
418
-
419
- def _get_directory(path: pathlib.Path) -> pathlib.Path:
420
- """Get the directory of a path - itself if already a directory."""
421
- if path.is_file():
422
- return path.parent
423
- else:
424
- return path
425
-
426
-
427
- def _get_legacy_hook_marks(
428
- method: Any,
429
- hook_type: str,
430
- opt_names: tuple[str, ...],
431
- ) -> dict[str, bool]:
432
- if TYPE_CHECKING:
433
- # abuse typeguard from importlib to avoid massive method type union that's lacking an alias
434
- assert inspect.isroutine(method)
435
- known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
436
- must_warn: list[str] = []
437
- opts: dict[str, bool] = {}
438
- for opt_name in opt_names:
439
- opt_attr = getattr(method, opt_name, AttributeError)
440
- if opt_attr is not AttributeError:
441
- must_warn.append(f"{opt_name}={opt_attr}")
442
- opts[opt_name] = True
443
- elif opt_name in known_marks:
444
- must_warn.append(f"{opt_name}=True")
445
- opts[opt_name] = True
446
- else:
447
- opts[opt_name] = False
448
- if must_warn:
449
- hook_opts = ", ".join(must_warn)
450
- message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
451
- type=hook_type,
452
- fullname=method.__qualname__,
453
- hook_opts=hook_opts,
454
- )
455
- warn_explicit_for(cast(FunctionType, method), message)
456
- return opts
457
-
458
-
459
- @final
460
- class PytestPluginManager(PluginManager):
461
- """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
462
- additional pytest-specific functionality:
463
-
464
- * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
465
- ``pytest_plugins`` global variables found in plugins being loaded.
466
- * ``conftest.py`` loading during start-up.
467
- """
468
-
469
- def __init__(self) -> None:
470
- from _pytest.assertion import DummyRewriteHook
471
- from _pytest.assertion import RewriteHook
472
-
473
- super().__init__("pytest")
474
-
475
- # -- State related to local conftest plugins.
476
- # All loaded conftest modules.
477
- self._conftest_plugins: set[types.ModuleType] = set()
478
- # All conftest modules applicable for a directory.
479
- # This includes the directory's own conftest modules as well
480
- # as those of its parent directories.
481
- self._dirpath2confmods: dict[pathlib.Path, list[types.ModuleType]] = {}
482
- # Cutoff directory above which conftests are no longer discovered.
483
- self._confcutdir: pathlib.Path | None = None
484
- # If set, conftest loading is skipped.
485
- self._noconftest = False
486
-
487
- # _getconftestmodules()'s call to _get_directory() causes a stat
488
- # storm when it's called potentially thousands of times in a test
489
- # session (#9478), often with the same path, so cache it.
490
- self._get_directory = lru_cache(256)(_get_directory)
491
-
492
- # plugins that were explicitly skipped with pytest.skip
493
- # list of (module name, skip reason)
494
- # previously we would issue a warning when a plugin was skipped, but
495
- # since we refactored warnings as first citizens of Config, they are
496
- # just stored here to be used later.
497
- self.skipped_plugins: list[tuple[str, str]] = []
498
-
499
- self.add_hookspecs(_pytest.hookspec)
500
- self.register(self)
501
- if os.environ.get("PYTEST_DEBUG"):
502
- err: IO[str] = sys.stderr
503
- encoding: str = getattr(err, "encoding", "utf8")
504
- try:
505
- err = open(
506
- os.dup(err.fileno()),
507
- mode=err.mode,
508
- buffering=1,
509
- encoding=encoding,
510
- )
511
- except Exception:
512
- pass
513
- self.trace.root.setwriter(err.write)
514
- self.enable_tracing()
515
-
516
- # Config._consider_importhook will set a real object if required.
517
- self.rewrite_hook: RewriteHook = DummyRewriteHook()
518
- # Used to know when we are importing conftests after the pytest_configure stage.
519
- self._configured = False
520
-
521
- def parse_hookimpl_opts(
522
- self, plugin: _PluggyPlugin, name: str
523
- ) -> HookimplOpts | None:
524
- """:meta private:"""
525
- # pytest hooks are always prefixed with "pytest_",
526
- # so we avoid accessing possibly non-readable attributes
527
- # (see issue #1073).
528
- if not name.startswith("pytest_"):
529
- return None
530
- # Ignore names which cannot be hooks.
531
- if name == "pytest_plugins":
532
- return None
533
-
534
- opts = super().parse_hookimpl_opts(plugin, name)
535
- if opts is not None:
536
- return opts
537
-
538
- method = getattr(plugin, name)
539
- # Consider only actual functions for hooks (#3775).
540
- if not inspect.isroutine(method):
541
- return None
542
- # Collect unmarked hooks as long as they have the `pytest_' prefix.
543
- legacy = _get_legacy_hook_marks(
544
- method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
545
- )
546
- return cast(HookimplOpts, legacy)
547
-
548
- def parse_hookspec_opts(self, module_or_class, name: str) -> HookspecOpts | None:
549
- """:meta private:"""
550
- opts = super().parse_hookspec_opts(module_or_class, name)
551
- if opts is None:
552
- method = getattr(module_or_class, name)
553
- if name.startswith("pytest_"):
554
- legacy = _get_legacy_hook_marks(
555
- method, "spec", ("firstresult", "historic")
556
- )
557
- opts = cast(HookspecOpts, legacy)
558
- return opts
559
-
560
- def register(self, plugin: _PluggyPlugin, name: str | None = None) -> str | None:
561
- if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
562
- warnings.warn(
563
- PytestConfigWarning(
564
- "{} plugin has been merged into the core, "
565
- "please remove it from your requirements.".format(
566
- name.replace("_", "-")
567
- )
568
- )
569
- )
570
- return None
571
- plugin_name = super().register(plugin, name)
572
- if plugin_name is not None:
573
- self.hook.pytest_plugin_registered.call_historic(
574
- kwargs=dict(
575
- plugin=plugin,
576
- plugin_name=plugin_name,
577
- manager=self,
578
- )
579
- )
580
-
581
- if isinstance(plugin, types.ModuleType):
582
- self.consider_module(plugin)
583
- return plugin_name
584
-
585
- def getplugin(self, name: str):
586
- # Support deprecated naming because plugins (xdist e.g.) use it.
587
- plugin: _PluggyPlugin | None = self.get_plugin(name)
588
- return plugin
589
-
590
- def hasplugin(self, name: str) -> bool:
591
- """Return whether a plugin with the given name is registered."""
592
- return bool(self.get_plugin(name))
593
-
594
- def pytest_configure(self, config: Config) -> None:
595
- """:meta private:"""
596
- # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
597
- # we should remove tryfirst/trylast as markers.
598
- config.addinivalue_line(
599
- "markers",
600
- "tryfirst: mark a hook implementation function such that the "
601
- "plugin machinery will try to call it first/as early as possible. "
602
- "DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.",
603
- )
604
- config.addinivalue_line(
605
- "markers",
606
- "trylast: mark a hook implementation function such that the "
607
- "plugin machinery will try to call it last/as late as possible. "
608
- "DEPRECATED, use @pytest.hookimpl(trylast=True) instead.",
609
- )
610
- self._configured = True
611
-
612
- #
613
- # Internal API for local conftest plugin handling.
614
- #
615
- def _set_initial_conftests(
616
- self,
617
- args: Sequence[str | pathlib.Path],
618
- pyargs: bool,
619
- noconftest: bool,
620
- rootpath: pathlib.Path,
621
- confcutdir: pathlib.Path | None,
622
- invocation_dir: pathlib.Path,
623
- importmode: ImportMode | str,
624
- *,
625
- consider_namespace_packages: bool,
626
- ) -> None:
627
- """Load initial conftest files given a preparsed "namespace".
628
-
629
- As conftest files may add their own command line options which have
630
- arguments ('--my-opt somepath') we might get some false positives.
631
- All builtin and 3rd party plugins will have been loaded, however, so
632
- common options will not confuse our logic here.
633
- """
634
- self._confcutdir = (
635
- absolutepath(invocation_dir / confcutdir) if confcutdir else None
636
- )
637
- self._noconftest = noconftest
638
- self._using_pyargs = pyargs
639
-
640
- anchors = []
641
- for initial_path in args:
642
- path = str(initial_path)
643
- # remove node-id syntax
644
- i = path.find("::")
645
- if i != -1:
646
- path = path[:i]
647
- anchor = absolutepath(invocation_dir / path)
648
- # Ensure we do not break if what appears to be an anchor
649
- # is in fact a very long option (#10169, #11394).
650
- if not safe_exists(anchor):
651
- continue
652
-
653
- anchors.append(anchor)
654
- # Let's also consider test* subdirs.
655
- if anchor.is_dir():
656
- anchors.extend(x for x in anchor.glob("test*") if x.is_dir())
657
- if not anchors:
658
- anchors.append(invocation_dir)
659
- anchors.extend(x for x in invocation_dir.glob("test*") if x.is_dir())
660
-
661
- for anchor in anchors:
662
- self._loadconftestmodules(
663
- anchor,
664
- importmode,
665
- rootpath,
666
- consider_namespace_packages=consider_namespace_packages,
667
- )
668
-
669
- def _is_in_confcutdir(self, path: pathlib.Path) -> bool:
670
- """Whether to consider the given path to load conftests from."""
671
- if self._confcutdir is None:
672
- return True
673
- # The semantics here are literally:
674
- # Do not load a conftest if it is found upwards from confcut dir.
675
- # But this is *not* the same as:
676
- # Load only conftests from confcutdir or below.
677
- # At first glance they might seem the same thing, however we do support use cases where
678
- # we want to load conftests that are not found in confcutdir or below, but are found
679
- # in completely different directory hierarchies like packages installed
680
- # in out-of-source trees.
681
- # (see #9767 for a regression where the logic was inverted).
682
- return path not in self._confcutdir.parents
683
-
684
- def _loadconftestmodules(
685
- self,
686
- path: pathlib.Path,
687
- importmode: str | ImportMode,
688
- rootpath: pathlib.Path,
689
- *,
690
- consider_namespace_packages: bool,
691
- ) -> None:
692
- if self._noconftest:
693
- return
694
-
695
- directory = self._get_directory(path)
696
-
697
- # Optimization: avoid repeated searches in the same directory.
698
- # Assumes always called with same importmode and rootpath.
699
- if directory in self._dirpath2confmods:
700
- return
701
-
702
- clist = []
703
- for parent in reversed((directory, *directory.parents)):
704
- if self._is_in_confcutdir(parent):
705
- conftestpath = parent / "conftest.py"
706
- if conftestpath.is_file():
707
- mod = self._importconftest(
708
- conftestpath,
709
- importmode,
710
- rootpath,
711
- consider_namespace_packages=consider_namespace_packages,
712
- )
713
- clist.append(mod)
714
- self._dirpath2confmods[directory] = clist
715
-
716
- def _getconftestmodules(self, path: pathlib.Path) -> Sequence[types.ModuleType]:
717
- directory = self._get_directory(path)
718
- return self._dirpath2confmods.get(directory, ())
719
-
720
- def _rget_with_confmod(
721
- self,
722
- name: str,
723
- path: pathlib.Path,
724
- ) -> tuple[types.ModuleType, Any]:
725
- modules = self._getconftestmodules(path)
726
- for mod in reversed(modules):
727
- try:
728
- return mod, getattr(mod, name)
729
- except AttributeError:
730
- continue
731
- raise KeyError(name)
732
-
733
- def _importconftest(
734
- self,
735
- conftestpath: pathlib.Path,
736
- importmode: str | ImportMode,
737
- rootpath: pathlib.Path,
738
- *,
739
- consider_namespace_packages: bool,
740
- ) -> types.ModuleType:
741
- conftestpath_plugin_name = str(conftestpath)
742
- existing = self.get_plugin(conftestpath_plugin_name)
743
- if existing is not None:
744
- return cast(types.ModuleType, existing)
745
-
746
- # conftest.py files there are not in a Python package all have module
747
- # name "conftest", and thus conflict with each other. Clear the existing
748
- # before loading the new one, otherwise the existing one will be
749
- # returned from the module cache.
750
- pkgpath = resolve_package_path(conftestpath)
751
- if pkgpath is None:
752
- try:
753
- del sys.modules[conftestpath.stem]
754
- except KeyError:
755
- pass
756
-
757
- try:
758
- mod = import_path(
759
- conftestpath,
760
- mode=importmode,
761
- root=rootpath,
762
- consider_namespace_packages=consider_namespace_packages,
763
- )
764
- except Exception as e:
765
- assert e.__traceback__ is not None
766
- raise ConftestImportFailure(conftestpath, cause=e) from e
767
-
768
- self._check_non_top_pytest_plugins(mod, conftestpath)
769
-
770
- self._conftest_plugins.add(mod)
771
- dirpath = conftestpath.parent
772
- if dirpath in self._dirpath2confmods:
773
- for path, mods in self._dirpath2confmods.items():
774
- if dirpath in path.parents or path == dirpath:
775
- if mod in mods:
776
- raise AssertionError(
777
- f"While trying to load conftest path {conftestpath!s}, "
778
- f"found that the module {mod} is already loaded with path {mod.__file__}. "
779
- "This is not supposed to happen. Please report this issue to pytest."
780
- )
781
- mods.append(mod)
782
- self.trace(f"loading conftestmodule {mod!r}")
783
- self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
784
- return mod
785
-
786
- def _check_non_top_pytest_plugins(
787
- self,
788
- mod: types.ModuleType,
789
- conftestpath: pathlib.Path,
790
- ) -> None:
791
- if (
792
- hasattr(mod, "pytest_plugins")
793
- and self._configured
794
- and not self._using_pyargs
795
- ):
796
- msg = (
797
- "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n"
798
- "It affects the entire test suite instead of just below the conftest as expected.\n"
799
- " {}\n"
800
- "Please move it to a top level conftest file at the rootdir:\n"
801
- " {}\n"
802
- "For more information, visit:\n"
803
- " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files"
804
- )
805
- fail(msg.format(conftestpath, self._confcutdir), pytrace=False)
806
-
807
- #
808
- # API for bootstrapping plugin loading
809
- #
810
- #
811
-
812
- def consider_preparse(
813
- self, args: Sequence[str], *, exclude_only: bool = False
814
- ) -> None:
815
- """:meta private:"""
816
- i = 0
817
- n = len(args)
818
- while i < n:
819
- opt = args[i]
820
- i += 1
821
- if isinstance(opt, str):
822
- if opt == "-p":
823
- try:
824
- parg = args[i]
825
- except IndexError:
826
- return
827
- i += 1
828
- elif opt.startswith("-p"):
829
- parg = opt[2:]
830
- else:
831
- continue
832
- parg = parg.strip()
833
- if exclude_only and not parg.startswith("no:"):
834
- continue
835
- self.consider_pluginarg(parg)
836
-
837
- def consider_pluginarg(self, arg: str) -> None:
838
- """:meta private:"""
839
- if arg.startswith("no:"):
840
- name = arg[3:]
841
- if name in essential_plugins:
842
- raise UsageError(f"plugin {name} cannot be disabled")
843
-
844
- if name.endswith("conftest.py"):
845
- raise UsageError(
846
- f"Blocking conftest files using -p is not supported: -p no:{name}\n"
847
- "conftest.py files are not plugins and cannot be disabled via -p.\n"
848
- )
849
-
850
- # PR #4304: remove stepwise if cacheprovider is blocked.
851
- if name == "cacheprovider":
852
- self.set_blocked("stepwise")
853
- self.set_blocked("pytest_stepwise")
854
-
855
- self.set_blocked(name)
856
- if not name.startswith("pytest_"):
857
- self.set_blocked("pytest_" + name)
858
- else:
859
- name = arg
860
- # Unblock the plugin.
861
- self.unblock(name)
862
- if not name.startswith("pytest_"):
863
- self.unblock("pytest_" + name)
864
- self.import_plugin(arg, consider_entry_points=True)
865
-
866
- def consider_conftest(
867
- self, conftestmodule: types.ModuleType, registration_name: str
868
- ) -> None:
869
- """:meta private:"""
870
- self.register(conftestmodule, name=registration_name)
871
-
872
- def consider_env(self) -> None:
873
- """:meta private:"""
874
- self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
875
-
876
- def consider_module(self, mod: types.ModuleType) -> None:
877
- """:meta private:"""
878
- self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
879
-
880
- def _import_plugin_specs(
881
- self, spec: None | types.ModuleType | str | Sequence[str]
882
- ) -> None:
883
- plugins = _get_plugin_specs_as_list(spec)
884
- for import_spec in plugins:
885
- self.import_plugin(import_spec)
886
-
887
- def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
888
- """Import a plugin with ``modname``.
889
-
890
- If ``consider_entry_points`` is True, entry point names are also
891
- considered to find a plugin.
892
- """
893
- # Most often modname refers to builtin modules, e.g. "pytester",
894
- # "terminal" or "capture". Those plugins are registered under their
895
- # basename for historic purposes but must be imported with the
896
- # _pytest prefix.
897
- assert isinstance(modname, str), (
898
- f"module name as text required, got {modname!r}"
899
- )
900
- if self.is_blocked(modname) or self.get_plugin(modname) is not None:
901
- return
902
-
903
- importspec = "_pytest." + modname if modname in builtin_plugins else modname
904
- self.rewrite_hook.mark_rewrite(importspec)
905
-
906
- if consider_entry_points:
907
- loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
908
- if loaded:
909
- return
910
-
911
- try:
912
- if sys.version_info >= (3, 11):
913
- mod = importlib.import_module(importspec)
914
- else:
915
- # On Python 3.10, import_module breaks
916
- # testing/test_config.py::test_disable_plugin_autoload.
917
- __import__(importspec)
918
- mod = sys.modules[importspec]
919
- except ImportError as e:
920
- raise ImportError(
921
- f'Error importing plugin "{modname}": {e.args[0]}'
922
- ).with_traceback(e.__traceback__) from e
923
-
924
- except Skipped as e:
925
- self.skipped_plugins.append((modname, e.msg or ""))
926
- else:
927
- self.register(mod, modname)
928
-
929
-
930
- def _get_plugin_specs_as_list(
931
- specs: None | types.ModuleType | str | Sequence[str],
932
- ) -> list[str]:
933
- """Parse a plugins specification into a list of plugin names."""
934
- # None means empty.
935
- if specs is None:
936
- return []
937
- # Workaround for #3899 - a submodule which happens to be called "pytest_plugins".
938
- if isinstance(specs, types.ModuleType):
939
- return []
940
- # Comma-separated list.
941
- if isinstance(specs, str):
942
- return specs.split(",") if specs else []
943
- # Direct specification.
944
- if isinstance(specs, collections.abc.Sequence):
945
- return list(specs)
946
- raise UsageError(
947
- f"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: {specs!r}"
948
- )
949
-
950
-
951
- def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
952
- """Given an iterable of file names in a source distribution, return the "names" that should
953
- be marked for assertion rewrite.
954
-
955
- For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
956
- the assertion rewrite mechanism.
957
-
958
- This function has to deal with dist-info based distributions and egg based distributions
959
- (which are still very much in use for "editable" installs).
960
-
961
- Here are the file names as seen in a dist-info based distribution:
962
-
963
- pytest_mock/__init__.py
964
- pytest_mock/_version.py
965
- pytest_mock/plugin.py
966
- pytest_mock.egg-info/PKG-INFO
967
-
968
- Here are the file names as seen in an egg based distribution:
969
-
970
- src/pytest_mock/__init__.py
971
- src/pytest_mock/_version.py
972
- src/pytest_mock/plugin.py
973
- src/pytest_mock.egg-info/PKG-INFO
974
- LICENSE
975
- setup.py
976
-
977
- We have to take in account those two distribution flavors in order to determine which
978
- names should be considered for assertion rewriting.
979
-
980
- More information:
981
- https://github.com/pytest-dev/pytest-mock/issues/167
982
- """
983
- package_files = list(package_files)
984
- seen_some = False
985
- for fn in package_files:
986
- is_simple_module = "/" not in fn and fn.endswith(".py")
987
- is_package = fn.count("/") == 1 and fn.endswith("__init__.py")
988
- if is_simple_module:
989
- module_name, _ = os.path.splitext(fn)
990
- # we ignore "setup.py" at the root of the distribution
991
- # as well as editable installation finder modules made by setuptools
992
- if module_name != "setup" and not module_name.startswith("__editable__"):
993
- seen_some = True
994
- yield module_name
995
- elif is_package:
996
- package_name = os.path.dirname(fn)
997
- seen_some = True
998
- yield package_name
999
-
1000
- if not seen_some:
1001
- # At this point we did not find any packages or modules suitable for assertion
1002
- # rewriting, so we try again by stripping the first path component (to account for
1003
- # "src" based source trees for example).
1004
- # This approach lets us have the common case continue to be fast, as egg-distributions
1005
- # are rarer.
1006
- new_package_files = []
1007
- for fn in package_files:
1008
- parts = fn.split("/")
1009
- new_fn = "/".join(parts[1:])
1010
- if new_fn:
1011
- new_package_files.append(new_fn)
1012
- if new_package_files:
1013
- yield from _iter_rewritable_modules(new_package_files)
1014
-
1015
-
1016
- class _DeprecatedInicfgProxy(MutableMapping[str, Any]):
1017
- """Compatibility proxy for the deprecated Config.inicfg."""
1018
-
1019
- __slots__ = ("_config",)
1020
-
1021
- def __init__(self, config: Config) -> None:
1022
- self._config = config
1023
-
1024
- def __getitem__(self, key: str) -> Any:
1025
- return self._config._inicfg[key].value
1026
-
1027
- def __setitem__(self, key: str, value: Any) -> None:
1028
- self._config._inicfg[key] = ConfigValue(value, origin="override", mode="toml")
1029
-
1030
- def __delitem__(self, key: str) -> None:
1031
- del self._config._inicfg[key]
1032
-
1033
- def __iter__(self) -> Iterator[str]:
1034
- return iter(self._config._inicfg)
1035
-
1036
- def __len__(self) -> int:
1037
- return len(self._config._inicfg)
1038
-
1039
-
1040
- @final
1041
- class Config:
1042
- """Access to configuration values, pluginmanager and plugin hooks.
1043
-
1044
- :param PytestPluginManager pluginmanager:
1045
- A pytest PluginManager.
1046
-
1047
- :param InvocationParams invocation_params:
1048
- Object containing parameters regarding the :func:`pytest.main`
1049
- invocation.
1050
- """
1051
-
1052
- @final
1053
- @dataclasses.dataclass(frozen=True)
1054
- class InvocationParams:
1055
- """Holds parameters passed during :func:`pytest.main`.
1056
-
1057
- The object attributes are read-only.
1058
-
1059
- .. versionadded:: 5.1
1060
-
1061
- .. note::
1062
-
1063
- Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts``
1064
- configuration option are handled by pytest, not being included in the ``args`` attribute.
1065
-
1066
- Plugins accessing ``InvocationParams`` must be aware of that.
1067
- """
1068
-
1069
- args: tuple[str, ...]
1070
- """The command-line arguments as passed to :func:`pytest.main`."""
1071
- plugins: Sequence[str | _PluggyPlugin] | None
1072
- """Extra plugins, might be `None`."""
1073
- dir: pathlib.Path
1074
- """The directory from which :func:`pytest.main` was invoked."""
1075
-
1076
- def __init__(
1077
- self,
1078
- *,
1079
- args: Iterable[str],
1080
- plugins: Sequence[str | _PluggyPlugin] | None,
1081
- dir: pathlib.Path,
1082
- ) -> None:
1083
- object.__setattr__(self, "args", tuple(args))
1084
- object.__setattr__(self, "plugins", plugins)
1085
- object.__setattr__(self, "dir", dir)
1086
-
1087
- class ArgsSource(enum.Enum):
1088
- """Indicates the source of the test arguments.
1089
-
1090
- .. versionadded:: 7.2
1091
- """
1092
-
1093
- #: Command line arguments.
1094
- ARGS = enum.auto()
1095
- #: Invocation directory.
1096
- INVOCATION_DIR = enum.auto()
1097
- INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
1098
- #: 'testpaths' configuration value.
1099
- TESTPATHS = enum.auto()
1100
-
1101
- # Set by cacheprovider plugin.
1102
- cache: Cache
1103
-
1104
- def __init__(
1105
- self,
1106
- pluginmanager: PytestPluginManager,
1107
- *,
1108
- invocation_params: InvocationParams | None = None,
1109
- prog: str | None = None,
1110
- ) -> None:
1111
- if invocation_params is None:
1112
- invocation_params = self.InvocationParams(
1113
- args=(), plugins=None, dir=pathlib.Path.cwd()
1114
- )
1115
-
1116
- self.option = argparse.Namespace()
1117
- """Access to command line option as attributes.
1118
-
1119
- :type: argparse.Namespace
1120
- """
1121
-
1122
- self.invocation_params = invocation_params
1123
- """The parameters with which pytest was invoked.
1124
-
1125
- :type: InvocationParams
1126
- """
1127
-
1128
- self._parser = Parser(
1129
- usage=f"%(prog)s [options] [{FILE_OR_DIR}] [{FILE_OR_DIR}] [...]",
1130
- processopt=self._processopt,
1131
- prog=prog,
1132
- _ispytest=True,
1133
- )
1134
- self.pluginmanager = pluginmanager
1135
- """The plugin manager handles plugin registration and hook invocation.
1136
-
1137
- :type: PytestPluginManager
1138
- """
1139
-
1140
- self.stash = Stash()
1141
- """A place where plugins can store information on the config for their
1142
- own use.
1143
-
1144
- :type: Stash
1145
- """
1146
- # Deprecated alias. Was never public. Can be removed in a few releases.
1147
- self._store = self.stash
1148
-
1149
- self.trace = self.pluginmanager.trace.root.get("config")
1150
- self.hook = self.pluginmanager.hook
1151
- self._inicache: dict[str, Any] = {}
1152
- self._inicfg: ConfigDict = {}
1153
- self._cleanup_stack = contextlib.ExitStack()
1154
- self.pluginmanager.register(self, "pytestconfig")
1155
- self._configured = False
1156
- self.hook.pytest_addoption.call_historic(
1157
- kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
1158
- )
1159
- self.args_source = Config.ArgsSource.ARGS
1160
- self.args: list[str] = []
1161
-
1162
- if TYPE_CHECKING:
1163
-
1164
- @deprecated(
1165
- "config.inicfg is deprecated, use config.getini() to access configuration values instead.",
1166
- )
1167
- @property
1168
- def inicfg(self) -> _DeprecatedInicfgProxy:
1169
- raise NotImplementedError()
1170
- else:
1171
-
1172
- @property
1173
- def inicfg(self) -> _DeprecatedInicfgProxy:
1174
- warnings.warn(
1175
- _pytest.deprecated.CONFIG_INICFG,
1176
- stacklevel=2,
1177
- )
1178
- return _DeprecatedInicfgProxy(self)
1179
-
1180
- @property
1181
- def rootpath(self) -> pathlib.Path:
1182
- """The path to the :ref:`rootdir <rootdir>`.
1183
-
1184
- .. versionadded:: 6.1
1185
- """
1186
- return self._rootpath
1187
-
1188
- @property
1189
- def inipath(self) -> pathlib.Path | None:
1190
- """The path to the :ref:`configfile <configfiles>`.
1191
-
1192
- .. versionadded:: 6.1
1193
- """
1194
- return self._inipath
1195
-
1196
- def add_cleanup(self, func: Callable[[], None]) -> None:
1197
- """Add a function to be called when the config object gets out of
1198
- use (usually coinciding with pytest_unconfigure).
1199
- """
1200
- self._cleanup_stack.callback(func)
1201
-
1202
- def _do_configure(self) -> None:
1203
- assert not self._configured
1204
- self._configured = True
1205
- self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
1206
-
1207
- def _ensure_unconfigure(self) -> None:
1208
- try:
1209
- if self._configured:
1210
- self._configured = False
1211
- try:
1212
- self.hook.pytest_unconfigure(config=self)
1213
- finally:
1214
- self.hook.pytest_configure._call_history = []
1215
- finally:
1216
- try:
1217
- self._cleanup_stack.close()
1218
- finally:
1219
- self._cleanup_stack = contextlib.ExitStack()
1220
-
1221
- def get_terminal_writer(self) -> TerminalWriter:
1222
- terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin(
1223
- "terminalreporter"
1224
- )
1225
- assert terminalreporter is not None
1226
- return terminalreporter._tw
1227
-
1228
- def pytest_cmdline_parse(
1229
- self, pluginmanager: PytestPluginManager, args: list[str]
1230
- ) -> Config:
1231
- try:
1232
- self.parse(args)
1233
- except UsageError:
1234
- # Handle `--version --version` and `--help` here in a minimal fashion.
1235
- # This gets done via helpconfig normally, but its
1236
- # pytest_cmdline_main is not called in case of errors.
1237
- if getattr(self.option, "version", False) or "--version" in args:
1238
- from _pytest.helpconfig import show_version_verbose
1239
-
1240
- # Note that `--version` (single argument) is handled early by `Config.main()`, so the only
1241
- # way we are reaching this point is via `--version --version`.
1242
- show_version_verbose(self)
1243
- elif (
1244
- getattr(self.option, "help", False) or "--help" in args or "-h" in args
1245
- ):
1246
- self._parser.optparser.print_help()
1247
- sys.stdout.write(
1248
- "\nNOTE: displaying only minimal help due to UsageError.\n\n"
1249
- )
1250
-
1251
- raise
1252
-
1253
- return self
1254
-
1255
- def notify_exception(
1256
- self,
1257
- excinfo: ExceptionInfo[BaseException],
1258
- option: argparse.Namespace | None = None,
1259
- ) -> None:
1260
- if option and getattr(option, "fulltrace", False):
1261
- style: TracebackStyle = "long"
1262
- else:
1263
- style = "native"
1264
- excrepr = excinfo.getrepr(
1265
- funcargs=True, showlocals=getattr(option, "showlocals", False), style=style
1266
- )
1267
- res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
1268
- if not any(res):
1269
- for line in str(excrepr).split("\n"):
1270
- sys.stderr.write(f"INTERNALERROR> {line}\n")
1271
- sys.stderr.flush()
1272
-
1273
- def cwd_relative_nodeid(self, nodeid: str) -> str:
1274
- # nodeid's are relative to the rootpath, compute relative to cwd.
1275
- if self.invocation_params.dir != self.rootpath:
1276
- base_path_part, *nodeid_part = nodeid.split("::")
1277
- # Only process path part
1278
- fullpath = self.rootpath / base_path_part
1279
- relative_path = bestrelpath(self.invocation_params.dir, fullpath)
1280
-
1281
- nodeid = "::".join([relative_path, *nodeid_part])
1282
- return nodeid
1283
-
1284
- @classmethod
1285
- def fromdictargs(cls, option_dict: Mapping[str, Any], args: list[str]) -> Config:
1286
- """Constructor usable for subprocesses."""
1287
- config = get_config(args)
1288
- config.option.__dict__.update(option_dict)
1289
- config.parse(args, addopts=False)
1290
- for x in config.option.plugins:
1291
- config.pluginmanager.consider_pluginarg(x)
1292
- return config
1293
-
1294
- def _processopt(self, opt: Argument) -> None:
1295
- if not hasattr(self.option, opt.dest):
1296
- setattr(self.option, opt.dest, opt.default)
1297
-
1298
- @hookimpl(trylast=True)
1299
- def pytest_load_initial_conftests(self, early_config: Config) -> None:
1300
- # We haven't fully parsed the command line arguments yet, so
1301
- # early_config.args it not set yet. But we need it for
1302
- # discovering the initial conftests. So "pre-run" the logic here.
1303
- # It will be done for real in `parse()`.
1304
- args, _args_source = early_config._decide_args(
1305
- args=early_config.known_args_namespace.file_or_dir,
1306
- pyargs=early_config.known_args_namespace.pyargs,
1307
- testpaths=early_config.getini("testpaths"),
1308
- invocation_dir=early_config.invocation_params.dir,
1309
- rootpath=early_config.rootpath,
1310
- warn=False,
1311
- )
1312
- self.pluginmanager._set_initial_conftests(
1313
- args=args,
1314
- pyargs=early_config.known_args_namespace.pyargs,
1315
- noconftest=early_config.known_args_namespace.noconftest,
1316
- rootpath=early_config.rootpath,
1317
- confcutdir=early_config.known_args_namespace.confcutdir,
1318
- invocation_dir=early_config.invocation_params.dir,
1319
- importmode=early_config.known_args_namespace.importmode,
1320
- consider_namespace_packages=early_config.getini(
1321
- "consider_namespace_packages"
1322
- ),
1323
- )
1324
-
1325
- def _consider_importhook(self) -> None:
1326
- """Install the PEP 302 import hook if using assertion rewriting.
1327
-
1328
- Needs to parse the --assert=<mode> option from the commandline
1329
- and find all the installed plugins to mark them for rewriting
1330
- by the importhook.
1331
- """
1332
- mode = getattr(self.known_args_namespace, "assertmode", "plain")
1333
-
1334
- disable_autoload = getattr(
1335
- self.known_args_namespace, "disable_plugin_autoload", False
1336
- ) or bool(os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"))
1337
- if mode == "rewrite":
1338
- import _pytest.assertion
1339
-
1340
- try:
1341
- hook = _pytest.assertion.install_importhook(self)
1342
- except SystemError:
1343
- mode = "plain"
1344
- else:
1345
- self._mark_plugins_for_rewrite(hook, disable_autoload)
1346
- self._warn_about_missing_assertion(mode)
1347
-
1348
- def _mark_plugins_for_rewrite(
1349
- self, hook: AssertionRewritingHook, disable_autoload: bool
1350
- ) -> None:
1351
- """Given an importhook, mark for rewrite any top-level
1352
- modules or packages in the distribution package for
1353
- all pytest plugins."""
1354
- self.pluginmanager.rewrite_hook = hook
1355
-
1356
- if disable_autoload:
1357
- # We don't autoload from distribution package entry points,
1358
- # no need to continue.
1359
- return
1360
-
1361
- package_files = (
1362
- str(file)
1363
- for dist in importlib.metadata.distributions()
1364
- if any(ep.group == "pytest11" for ep in dist.entry_points)
1365
- for file in dist.files or []
1366
- )
1367
-
1368
- for name in _iter_rewritable_modules(package_files):
1369
- hook.mark_rewrite(name)
1370
-
1371
- def _configure_python_path(self) -> None:
1372
- # `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]`
1373
- for path in reversed(self.getini("pythonpath")):
1374
- sys.path.insert(0, str(path))
1375
- self.add_cleanup(self._unconfigure_python_path)
1376
-
1377
- def _unconfigure_python_path(self) -> None:
1378
- for path in self.getini("pythonpath"):
1379
- path_str = str(path)
1380
- if path_str in sys.path:
1381
- sys.path.remove(path_str)
1382
-
1383
- def _validate_args(self, args: list[str], via: str) -> list[str]:
1384
- """Validate known args."""
1385
- self._parser.extra_info["config source"] = via
1386
- try:
1387
- self._parser.parse_known_and_unknown_args(
1388
- args, namespace=copy.copy(self.option)
1389
- )
1390
- finally:
1391
- self._parser.extra_info.pop("config source", None)
1392
-
1393
- return args
1394
-
1395
- def _decide_args(
1396
- self,
1397
- *,
1398
- args: list[str],
1399
- pyargs: bool,
1400
- testpaths: list[str],
1401
- invocation_dir: pathlib.Path,
1402
- rootpath: pathlib.Path,
1403
- warn: bool,
1404
- ) -> tuple[list[str], ArgsSource]:
1405
- """Decide the args (initial paths/nodeids) to use given the relevant inputs.
1406
-
1407
- :param warn: Whether can issue warnings.
1408
-
1409
- :returns: The args and the args source. Guaranteed to be non-empty.
1410
- """
1411
- if args:
1412
- source = Config.ArgsSource.ARGS
1413
- result = args
1414
- else:
1415
- if invocation_dir == rootpath:
1416
- source = Config.ArgsSource.TESTPATHS
1417
- if pyargs:
1418
- result = testpaths
1419
- else:
1420
- result = []
1421
- for path in testpaths:
1422
- result.extend(sorted(glob.iglob(path, recursive=True)))
1423
- if testpaths and not result:
1424
- if warn:
1425
- warning_text = (
1426
- "No files were found in testpaths; "
1427
- "consider removing or adjusting your testpaths configuration. "
1428
- "Searching recursively from the current directory instead."
1429
- )
1430
- self.issue_config_time_warning(
1431
- PytestConfigWarning(warning_text), stacklevel=3
1432
- )
1433
- else:
1434
- result = []
1435
- if not result:
1436
- source = Config.ArgsSource.INVOCATION_DIR
1437
- result = [str(invocation_dir)]
1438
- return result, source
1439
-
1440
- @hookimpl(wrapper=True)
1441
- def pytest_collection(self) -> Generator[None, object, object]:
1442
- # Validate invalid configuration keys after collection is done so we
1443
- # take in account options added by late-loading conftest files.
1444
- try:
1445
- return (yield)
1446
- finally:
1447
- self._validate_config_options()
1448
-
1449
- def _checkversion(self) -> None:
1450
- import pytest
1451
-
1452
- minver = self.getini("minversion")
1453
- if minver:
1454
- # Imported lazily to improve start-up time.
1455
- from packaging.version import Version
1456
-
1457
- if Version(minver) > Version(pytest.__version__):
1458
- raise pytest.UsageError(
1459
- f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
1460
- )
1461
-
1462
- def _validate_config_options(self) -> None:
1463
- for key in sorted(self._get_unknown_ini_keys()):
1464
- self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")
1465
-
1466
- def _validate_plugins(self) -> None:
1467
- required_plugins = sorted(self.getini("required_plugins"))
1468
- if not required_plugins:
1469
- return
1470
-
1471
- # Imported lazily to improve start-up time.
1472
- from packaging.requirements import InvalidRequirement
1473
- from packaging.requirements import Requirement
1474
- from packaging.version import Version
1475
-
1476
- plugin_info = self.pluginmanager.list_plugin_distinfo()
1477
- plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
1478
-
1479
- missing_plugins = []
1480
- for required_plugin in required_plugins:
1481
- try:
1482
- req = Requirement(required_plugin)
1483
- except InvalidRequirement:
1484
- missing_plugins.append(required_plugin)
1485
- continue
1486
-
1487
- if req.name not in plugin_dist_info:
1488
- missing_plugins.append(required_plugin)
1489
- elif not req.specifier.contains(
1490
- Version(plugin_dist_info[req.name]), prereleases=True
1491
- ):
1492
- missing_plugins.append(required_plugin)
1493
-
1494
- if missing_plugins:
1495
- raise UsageError(
1496
- "Missing required plugins: {}".format(", ".join(missing_plugins)),
1497
- )
1498
-
1499
- def _warn_or_fail_if_strict(self, message: str) -> None:
1500
- strict_config = self.getini("strict_config")
1501
- if strict_config is None:
1502
- strict_config = self.getini("strict")
1503
- if strict_config:
1504
- raise UsageError(message)
1505
-
1506
- self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
1507
-
1508
- def _get_unknown_ini_keys(self) -> set[str]:
1509
- known_keys = self._parser._inidict.keys() | self._parser._ini_aliases.keys()
1510
- return self._inicfg.keys() - known_keys
1511
-
1512
- def parse(self, args: list[str], addopts: bool = True) -> None:
1513
- # Parse given cmdline arguments into this config object.
1514
- assert self.args == [], (
1515
- "can only parse cmdline args at most once per Config object"
1516
- )
1517
-
1518
- self.hook.pytest_addhooks.call_historic(
1519
- kwargs=dict(pluginmanager=self.pluginmanager)
1520
- )
1521
-
1522
- if addopts:
1523
- env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
1524
- if len(env_addopts):
1525
- args[:] = (
1526
- self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
1527
- + args
1528
- )
1529
-
1530
- # At this point, self.option contains only defaults from the _processopt
1531
- # callback.
1532
- ns = self._parser.parse_known_args(args, namespace=copy.copy(self.option))
1533
- rootpath, inipath, inicfg, ignored_config_files = determine_setup(
1534
- inifile=ns.inifilename,
1535
- override_ini=ns.override_ini,
1536
- args=ns.file_or_dir,
1537
- rootdir_cmd_arg=ns.rootdir or None,
1538
- invocation_dir=self.invocation_params.dir,
1539
- )
1540
- self._rootpath = rootpath
1541
- self._inipath = inipath
1542
- self._ignored_config_files = ignored_config_files
1543
- self._inicfg = inicfg
1544
- self._parser.extra_info["rootdir"] = str(self.rootpath)
1545
- self._parser.extra_info["inifile"] = str(self.inipath)
1546
-
1547
- self._parser.addini("addopts", "Extra command line options", "args")
1548
- self._parser.addini("minversion", "Minimally required pytest version")
1549
- self._parser.addini(
1550
- "pythonpath", type="paths", help="Add paths to sys.path", default=[]
1551
- )
1552
- self._parser.addini(
1553
- "required_plugins",
1554
- "Plugins that must be present for pytest to run",
1555
- type="args",
1556
- default=[],
1557
- )
1558
-
1559
- if addopts:
1560
- args[:] = (
1561
- self._validate_args(self.getini("addopts"), "via addopts config") + args
1562
- )
1563
-
1564
- self.known_args_namespace = self._parser.parse_known_args(
1565
- args, namespace=copy.copy(self.option)
1566
- )
1567
- if addopts:
1568
- # addopts may have added overrides (especially via OverrideIniAction).
1569
- # The thing can be endlessly circular but we only do one level (#14442).
1570
- if overrides := parse_override_ini(self.known_args_namespace.override_ini):
1571
- self._inicfg.update(overrides)
1572
- self._inicache.clear()
1573
- self._checkversion()
1574
- self._consider_importhook()
1575
- self._configure_python_path()
1576
- self.pluginmanager.consider_preparse(args, exclude_only=False)
1577
- if (
1578
- not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
1579
- and not self.known_args_namespace.disable_plugin_autoload
1580
- ):
1581
- # Autoloading from distribution package entry point has
1582
- # not been disabled.
1583
- self.pluginmanager.load_setuptools_entrypoints("pytest11")
1584
- # Otherwise only plugins explicitly specified in PYTEST_PLUGINS
1585
- # are going to be loaded.
1586
- self.pluginmanager.consider_env()
1587
-
1588
- # Parse again, now including options added in pytest_addoption
1589
- # by third-party plugins loaded above. This way they're available
1590
- # on early_config in the pytest_load_initial_conftests hook call below.
1591
- self.known_args_namespace = self._parser.parse_known_args(
1592
- args, namespace=copy.copy(self.option)
1593
- )
1594
-
1595
- self._validate_plugins()
1596
- self._warn_about_skipped_plugins()
1597
-
1598
- if self.known_args_namespace.confcutdir is None:
1599
- if self.inipath is not None:
1600
- confcutdir = str(self.inipath.parent)
1601
- else:
1602
- confcutdir = str(self.rootpath)
1603
- self.known_args_namespace.confcutdir = confcutdir
1604
- try:
1605
- self.hook.pytest_load_initial_conftests(
1606
- early_config=self, args=args, parser=self._parser
1607
- )
1608
- except ConftestImportFailure as e:
1609
- if self.known_args_namespace.help or self.known_args_namespace.version:
1610
- # we don't want to prevent --help/--version to work
1611
- # so just let it pass and print a warning at the end
1612
- self.issue_config_time_warning(
1613
- PytestConfigWarning(f"could not load initial conftests: {e.path}"),
1614
- stacklevel=2,
1615
- )
1616
- else:
1617
- raise
1618
-
1619
- try:
1620
- self._parser.parse(args, namespace=self.option)
1621
- except PrintHelp:
1622
- return
1623
-
1624
- self.args, self.args_source = self._decide_args(
1625
- args=getattr(self.option, FILE_OR_DIR),
1626
- pyargs=self.option.pyargs,
1627
- testpaths=self.getini("testpaths"),
1628
- invocation_dir=self.invocation_params.dir,
1629
- rootpath=self.rootpath,
1630
- warn=True,
1631
- )
1632
-
1633
- def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
1634
- """Issue and handle a warning during the "configure" stage.
1635
-
1636
- During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
1637
- function because it is not possible to have hook wrappers around ``pytest_configure``.
1638
-
1639
- This function is mainly intended for plugins that need to issue warnings during
1640
- ``pytest_configure`` (or similar stages).
1641
-
1642
- :param warning: The warning instance.
1643
- :param stacklevel: stacklevel forwarded to warnings.warn.
1644
- """
1645
- if self.pluginmanager.is_blocked("warnings"):
1646
- return
1647
-
1648
- cmdline_filters = self.known_args_namespace.pythonwarnings or []
1649
- config_filters = self.getini("filterwarnings")
1650
-
1651
- with warnings.catch_warnings(record=True) as records:
1652
- warnings.simplefilter("always", type(warning))
1653
- apply_warning_filters(config_filters, cmdline_filters)
1654
- warnings.warn(warning, stacklevel=stacklevel)
1655
-
1656
- if records:
1657
- frame = sys._getframe(stacklevel - 1)
1658
- location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
1659
- self.hook.pytest_warning_recorded.call_historic(
1660
- kwargs=dict(
1661
- warning_message=records[0],
1662
- when="config",
1663
- nodeid="",
1664
- location=location,
1665
- )
1666
- )
1667
-
1668
- def addinivalue_line(self, name: str, line: str) -> None:
1669
- """Add a line to a configuration option. The option must have been
1670
- declared but might not yet be set in which case the line becomes
1671
- the first line in its value."""
1672
- x = self.getini(name)
1673
- assert isinstance(x, list)
1674
- x.append(line) # modifies the cached list inline
1675
-
1676
- def getini(self, name: str) -> Any:
1677
- """Return configuration value the an :ref:`configuration file <configfiles>`.
1678
-
1679
- If a configuration value is not defined in a
1680
- :ref:`configuration file <configfiles>`, then the ``default`` value
1681
- provided while registering the configuration through
1682
- :func:`parser.addini <pytest.Parser.addini>` will be returned.
1683
- Please note that you can even provide ``None`` as a valid
1684
- default value.
1685
-
1686
- If ``default`` is not provided while registering using
1687
- :func:`parser.addini <pytest.Parser.addini>`, then a default value
1688
- based on the ``type`` parameter passed to
1689
- :func:`parser.addini <pytest.Parser.addini>` will be returned.
1690
- The default values based on ``type`` are:
1691
- ``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
1692
- ``bool`` : ``False``
1693
- ``string`` : empty string ``""``
1694
- ``int`` : ``0``
1695
- ``float`` : ``0.0``
1696
-
1697
- If neither the ``default`` nor the ``type`` parameter is passed
1698
- while registering the configuration through
1699
- :func:`parser.addini <pytest.Parser.addini>`, then the configuration
1700
- is treated as a string and a default empty string '' is returned.
1701
-
1702
- If the specified name hasn't been registered through a prior
1703
- :func:`parser.addini <pytest.Parser.addini>` call (usually from a
1704
- plugin), a ValueError is raised.
1705
- """
1706
- canonical_name = self._parser._ini_aliases.get(name, name)
1707
- try:
1708
- return self._inicache[canonical_name]
1709
- except KeyError:
1710
- pass
1711
- self._inicache[canonical_name] = val = self._getini(canonical_name)
1712
- return val
1713
-
1714
- # Meant for easy monkeypatching by legacypath plugin.
1715
- # Can be inlined back (with no cover removed) once legacypath is gone.
1716
- def _getini_unknown_type(self, name: str, type: str, value: object):
1717
- msg = (
1718
- f"Option {name} has unknown configuration type {type} with value {value!r}"
1719
- )
1720
- raise ValueError(msg) # pragma: no cover
1721
-
1722
- def _getini(self, name: str):
1723
- # If this is an alias, resolve to canonical name.
1724
- canonical_name = self._parser._ini_aliases.get(name, name)
1725
-
1726
- try:
1727
- _description, type, default = self._parser._inidict[canonical_name]
1728
- except KeyError as e:
1729
- raise ValueError(f"unknown configuration value: {name!r}") from e
1730
-
1731
- # Collect all possible values (canonical name + aliases) from _inicfg.
1732
- # Each candidate is (ConfigValue, is_canonical).
1733
- candidates = []
1734
- if canonical_name in self._inicfg:
1735
- candidates.append((self._inicfg[canonical_name], True))
1736
- for alias, target in self._parser._ini_aliases.items():
1737
- if target == canonical_name and alias in self._inicfg:
1738
- candidates.append((self._inicfg[alias], False))
1739
-
1740
- if not candidates:
1741
- return default
1742
-
1743
- # Pick the best candidate based on precedence:
1744
- # 1. CLI override takes precedence over file, then
1745
- # 2. Canonical name takes precedence over alias.
1746
- selected = max(candidates, key=lambda x: (x[0].origin == "override", x[1]))[0]
1747
- value = selected.value
1748
- mode = selected.mode
1749
-
1750
- if mode == "ini":
1751
- # In ini mode, values are always str | list[str].
1752
- assert isinstance(value, (str, list))
1753
- return self._getini_ini(name, canonical_name, type, value, default)
1754
- elif mode == "toml":
1755
- return self._getini_toml(name, canonical_name, type, value, default)
1756
- else:
1757
- assert_never(mode)
1758
-
1759
- def _getini_ini(
1760
- self,
1761
- name: str,
1762
- canonical_name: str,
1763
- type: str,
1764
- value: str | list[str],
1765
- default: Any,
1766
- ):
1767
- """Handle config values read in INI mode.
1768
-
1769
- In INI mode, values are stored as str or list[str] only, and coerced
1770
- from string based on the registered type.
1771
- """
1772
- # Note: some coercions are only required if we are reading from .ini
1773
- # files, because the file format doesn't contain type information, but
1774
- # when reading from toml (in ini mode) we will get either str or list of
1775
- # str values (see load_config_dict_from_file). For example:
1776
- #
1777
- # ini:
1778
- # a_line_list = "tests acceptance"
1779
- #
1780
- # in this case, we need to split the string to obtain a list of strings.
1781
- #
1782
- # toml (ini mode):
1783
- # a_line_list = ["tests", "acceptance"]
1784
- #
1785
- # in this case, we already have a list ready to use.
1786
- if type == "paths":
1787
- dp = (
1788
- self.inipath.parent
1789
- if self.inipath is not None
1790
- else self.invocation_params.dir
1791
- )
1792
- input_values = shlex.split(value) if isinstance(value, str) else value
1793
- return [dp / x for x in input_values]
1794
- elif type == "args":
1795
- return shlex.split(value) if isinstance(value, str) else value
1796
- elif type == "linelist":
1797
- if isinstance(value, str):
1798
- return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
1799
- else:
1800
- return value
1801
- elif type == "bool":
1802
- return _strtobool(str(value).strip())
1803
- elif type == "string":
1804
- return value
1805
- elif type == "int":
1806
- if not isinstance(value, str):
1807
- raise TypeError(
1808
- f"Expected an int string for option {name} of type integer, but got: {value!r}"
1809
- ) from None
1810
- return int(value)
1811
- elif type == "float":
1812
- if not isinstance(value, str):
1813
- raise TypeError(
1814
- f"Expected a float string for option {name} of type float, but got: {value!r}"
1815
- ) from None
1816
- return float(value)
1817
- else:
1818
- return self._getini_unknown_type(name, type, value)
1819
-
1820
- def _getini_toml(
1821
- self,
1822
- name: str,
1823
- canonical_name: str,
1824
- type: str,
1825
- value: object,
1826
- default: Any,
1827
- ):
1828
- """Handle TOML config values with strict type validation and no coercion.
1829
-
1830
- In TOML mode, values already have native types from TOML parsing.
1831
- We validate types match expectations exactly, including list items.
1832
- """
1833
- value_type = builtins.type(value).__name__
1834
- if type == "paths":
1835
- # Expect a list of strings.
1836
- if not isinstance(value, list):
1837
- raise TypeError(
1838
- f"{self.inipath}: config option '{name}' expects a list for type 'paths', "
1839
- f"got {value_type}: {value!r}"
1840
- )
1841
- for i, item in enumerate(value):
1842
- if not isinstance(item, str):
1843
- item_type = builtins.type(item).__name__
1844
- raise TypeError(
1845
- f"{self.inipath}: config option '{name}' expects a list of strings, "
1846
- f"but item at index {i} is {item_type}: {item!r}"
1847
- )
1848
- dp = (
1849
- self.inipath.parent
1850
- if self.inipath is not None
1851
- else self.invocation_params.dir
1852
- )
1853
- return [dp / x for x in value]
1854
- elif type in {"args", "linelist"}:
1855
- # Expect a list of strings.
1856
- if not isinstance(value, list):
1857
- raise TypeError(
1858
- f"{self.inipath}: config option '{name}' expects a list for type '{type}', "
1859
- f"got {value_type}: {value!r}"
1860
- )
1861
- for i, item in enumerate(value):
1862
- if not isinstance(item, str):
1863
- item_type = builtins.type(item).__name__
1864
- raise TypeError(
1865
- f"{self.inipath}: config option '{name}' expects a list of strings, "
1866
- f"but item at index {i} is {item_type}: {item!r}"
1867
- )
1868
- return list(value)
1869
- elif type == "bool":
1870
- # Expect a boolean.
1871
- if not isinstance(value, bool):
1872
- raise TypeError(
1873
- f"{self.inipath}: config option '{name}' expects a bool, "
1874
- f"got {value_type}: {value!r}"
1875
- )
1876
- return value
1877
- elif type == "int":
1878
- # Expect an integer (but not bool, which is a subclass of int).
1879
- if not isinstance(value, int) or isinstance(value, bool):
1880
- raise TypeError(
1881
- f"{self.inipath}: config option '{name}' expects an int, "
1882
- f"got {value_type}: {value!r}"
1883
- )
1884
- return value
1885
- elif type == "float":
1886
- # Expect a float or integer only.
1887
- if not isinstance(value, (float, int)) or isinstance(value, bool):
1888
- raise TypeError(
1889
- f"{self.inipath}: config option '{name}' expects a float, "
1890
- f"got {value_type}: {value!r}"
1891
- )
1892
- return value
1893
- elif type == "string":
1894
- # Expect a string.
1895
- if not isinstance(value, str):
1896
- raise TypeError(
1897
- f"{self.inipath}: config option '{name}' expects a string, "
1898
- f"got {value_type}: {value!r}"
1899
- )
1900
- return value
1901
- else:
1902
- return self._getini_unknown_type(name, type, value)
1903
-
1904
- def _getconftest_pathlist(
1905
- self, name: str, path: pathlib.Path
1906
- ) -> list[pathlib.Path] | None:
1907
- try:
1908
- mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
1909
- except KeyError:
1910
- return None
1911
- assert mod.__file__ is not None
1912
- modpath = pathlib.Path(mod.__file__).parent
1913
- values: list[pathlib.Path] = []
1914
- for relroot in relroots:
1915
- if isinstance(relroot, os.PathLike):
1916
- relroot = pathlib.Path(relroot)
1917
- else:
1918
- relroot = relroot.replace("/", os.sep)
1919
- relroot = absolutepath(modpath / relroot)
1920
- values.append(relroot)
1921
- return values
1922
-
1923
- def getoption(self, name: str, default: Any = NOTSET, skip: bool = False):
1924
- """Return command line option value.
1925
-
1926
- :param name: Name of the option. You may also specify
1927
- the literal ``--OPT`` option instead of the "dest" option name.
1928
- :param default: Fallback value if no option of that name is **declared** via :hook:`pytest_addoption`.
1929
- Note this parameter will be ignored when the option is **declared** even if the option's value is ``None``.
1930
- :param skip: If ``True``, raise :func:`pytest.skip` if option is undeclared or has a ``None`` value.
1931
- Note that even if ``True``, if a default was specified it will be returned instead of a skip.
1932
- """
1933
- name = self._parser._opt2dest.get(name, name)
1934
- try:
1935
- val = getattr(self.option, name)
1936
- if val is None and skip:
1937
- raise AttributeError(name)
1938
- return val
1939
- except AttributeError as e:
1940
- if default is not NOTSET:
1941
- return default
1942
- if skip:
1943
- import pytest
1944
-
1945
- pytest.skip(f"no {name!r} option found")
1946
- raise ValueError(f"no option named {name!r}") from e
1947
-
1948
- def getvalue(self, name: str, path=None):
1949
- """Deprecated, use getoption() instead."""
1950
- return self.getoption(name)
1951
-
1952
- def getvalueorskip(self, name: str, path=None):
1953
- """Deprecated, use getoption(skip=True) instead."""
1954
- return self.getoption(name, skip=True)
1955
-
1956
- #: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
1957
- VERBOSITY_ASSERTIONS: Final = "assertions"
1958
- #: Verbosity type for test case execution (see :confval:`verbosity_test_cases`).
1959
- VERBOSITY_TEST_CASES: Final = "test_cases"
1960
- #: Verbosity type for failed subtests (see :confval:`verbosity_subtests`).
1961
- VERBOSITY_SUBTESTS: Final = "subtests"
1962
-
1963
- _VERBOSITY_INI_DEFAULT: Final = "auto"
1964
-
1965
- def get_verbosity(self, verbosity_type: str | None = None) -> int:
1966
- r"""Retrieve the verbosity level for a fine-grained verbosity type.
1967
-
1968
- :param verbosity_type: Verbosity type to get level for. If a level is
1969
- configured for the given type, that value will be returned. If the
1970
- given type is not a known verbosity type, the global verbosity
1971
- level will be returned. If the given type is None (default), the
1972
- global verbosity level will be returned.
1973
-
1974
- To configure a level for a fine-grained verbosity type, the
1975
- configuration file should have a setting for the configuration name
1976
- and a numeric value for the verbosity level. A special value of "auto"
1977
- can be used to explicitly use the global verbosity level.
1978
-
1979
- Example:
1980
-
1981
- .. tab:: toml
1982
-
1983
- .. code-block:: toml
1984
-
1985
- [tool.pytest]
1986
- verbosity_assertions = 2
1987
-
1988
- .. tab:: ini
1989
-
1990
- .. code-block:: ini
1991
-
1992
- [pytest]
1993
- verbosity_assertions = 2
1994
-
1995
- .. code-block:: console
1996
-
1997
- pytest -v
1998
-
1999
- .. code-block:: python
2000
-
2001
- print(config.get_verbosity()) # 1
2002
- print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
2003
- """
2004
- global_level = self.getoption("verbose", default=0)
2005
- assert isinstance(global_level, int)
2006
- if verbosity_type is None:
2007
- return global_level
2008
-
2009
- ini_name = Config._verbosity_ini_name(verbosity_type)
2010
- if ini_name not in self._parser._inidict:
2011
- return global_level
2012
-
2013
- level = self.getini(ini_name)
2014
- if level == Config._VERBOSITY_INI_DEFAULT:
2015
- return global_level
2016
-
2017
- return int(level)
2018
-
2019
- @staticmethod
2020
- def _verbosity_ini_name(verbosity_type: str) -> str:
2021
- return f"verbosity_{verbosity_type}"
2022
-
2023
- @staticmethod
2024
- def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None:
2025
- """Add a output verbosity configuration option for the given output type.
2026
-
2027
- :param parser: Parser for command line arguments and config-file values.
2028
- :param verbosity_type: Fine-grained verbosity category.
2029
- :param help: Description of the output this type controls.
2030
-
2031
- The value should be retrieved via a call to
2032
- :py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`.
2033
- """
2034
- parser.addini(
2035
- Config._verbosity_ini_name(verbosity_type),
2036
- help=help,
2037
- type="string",
2038
- default=Config._VERBOSITY_INI_DEFAULT,
2039
- )
2040
-
2041
- def _warn_about_missing_assertion(self, mode: str) -> None:
2042
- if not _assertion_supported():
2043
- if mode == "plain":
2044
- warning_text = (
2045
- "ASSERTIONS ARE NOT EXECUTED"
2046
- " and FAILING TESTS WILL PASS. Are you"
2047
- " using python -O?"
2048
- )
2049
- else:
2050
- warning_text = (
2051
- "assertions not in test modules or"
2052
- " plugins will be ignored"
2053
- " because assert statements are not executed "
2054
- "by the underlying Python interpreter "
2055
- "(are you using python -O?)\n"
2056
- )
2057
- self.issue_config_time_warning(
2058
- PytestConfigWarning(warning_text),
2059
- stacklevel=3,
2060
- )
2061
-
2062
- def _warn_about_skipped_plugins(self) -> None:
2063
- for module_name, msg in self.pluginmanager.skipped_plugins:
2064
- self.issue_config_time_warning(
2065
- PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"),
2066
- stacklevel=2,
2067
- )
2068
-
2069
-
2070
- def _assertion_supported() -> bool:
2071
- try:
2072
- assert False
2073
- except AssertionError:
2074
- return True
2075
- else:
2076
- return False # type: ignore[unreachable]
2077
-
2078
-
2079
- def create_terminal_writer(
2080
- config: Config, file: TextIO | None = None
2081
- ) -> TerminalWriter:
2082
- """Create a TerminalWriter instance configured according to the options
2083
- in the config object.
2084
-
2085
- Every code which requires a TerminalWriter object and has access to a
2086
- config object should use this function.
2087
- """
2088
- tw = TerminalWriter(file=file)
2089
-
2090
- if config.option.color == "yes":
2091
- tw.hasmarkup = True
2092
- elif config.option.color == "no":
2093
- tw.hasmarkup = False
2094
-
2095
- if config.option.code_highlight == "yes":
2096
- tw.code_highlight = True
2097
- elif config.option.code_highlight == "no":
2098
- tw.code_highlight = False
2099
-
2100
- return tw
2101
-
2102
-
2103
- def _strtobool(val: str) -> bool:
2104
- """Convert a string representation of truth to True or False.
2105
-
2106
- True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
2107
- are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
2108
- 'val' is anything else.
2109
-
2110
- .. note:: Copied from distutils.util.
2111
- """
2112
- val = val.lower()
2113
- if val in ("y", "yes", "t", "true", "on", "1"):
2114
- return True
2115
- elif val in ("n", "no", "f", "false", "off", "0"):
2116
- return False
2117
- else:
2118
- raise ValueError(f"invalid truth value {val!r}")
2119
-
2120
-
2121
- @lru_cache(maxsize=50)
2122
- def parse_warning_filter(
2123
- arg: str, *, escape: bool
2124
- ) -> tuple[warnings._ActionKind, str, type[Warning], str, int]:
2125
- """Parse a warnings filter string.
2126
-
2127
- This is copied from warnings._setoption with the following changes:
2128
-
2129
- * Does not apply the filter.
2130
- * Escaping is optional.
2131
- * Raises UsageError so we get nice error messages on failure.
2132
- """
2133
- __tracebackhide__ = True
2134
- error_template = dedent(
2135
- f"""\
2136
- while parsing the following warning configuration:
2137
-
2138
- {arg}
2139
-
2140
- This error occurred:
2141
-
2142
- {{error}}
2143
- """
2144
- )
2145
-
2146
- parts = arg.split(":")
2147
- if len(parts) > 5:
2148
- doc_url = (
2149
- "https://docs.python.org/3/library/warnings.html#describing-warning-filters"
2150
- )
2151
- error = dedent(
2152
- f"""\
2153
- Too many fields ({len(parts)}), expected at most 5 separated by colons:
2154
-
2155
- action:message:category:module:line
2156
-
2157
- For more information please consult: {doc_url}
2158
- """
2159
- )
2160
- raise UsageError(error_template.format(error=error))
2161
-
2162
- while len(parts) < 5:
2163
- parts.append("")
2164
- action_, message, category_, module, lineno_ = (s.strip() for s in parts)
2165
- try:
2166
- action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined]
2167
- except warnings._OptionError as e:
2168
- raise UsageError(error_template.format(error=str(e))) from None
2169
- try:
2170
- category: type[Warning] = _resolve_warning_category(category_)
2171
- except ImportError:
2172
- raise
2173
- except Exception:
2174
- exc_info = ExceptionInfo.from_current()
2175
- exception_text = exc_info.getrepr(style="native")
2176
- raise UsageError(error_template.format(error=exception_text)) from None
2177
- if message and escape:
2178
- message = re.escape(message)
2179
- if module and escape:
2180
- module = re.escape(module) + r"\Z"
2181
- if lineno_:
2182
- try:
2183
- lineno = int(lineno_)
2184
- if lineno < 0:
2185
- raise ValueError("number is negative")
2186
- except ValueError as e:
2187
- raise UsageError(
2188
- error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
2189
- ) from None
2190
- else:
2191
- lineno = 0
2192
- try:
2193
- re.compile(message)
2194
- re.compile(module)
2195
- except re.error as e:
2196
- raise UsageError(
2197
- error_template.format(error=f"Invalid regex {e.pattern!r}: {e}")
2198
- ) from None
2199
- return action, message, category, module, lineno
2200
-
2201
-
2202
- def _resolve_warning_category(category: str) -> type[Warning]:
2203
- """
2204
- Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)
2205
- propagate so we can get access to their tracebacks (#9218).
2206
- """
2207
- __tracebackhide__ = True
2208
- if not category:
2209
- return Warning
2210
-
2211
- if "." not in category:
2212
- import builtins as m
2213
-
2214
- klass = category
2215
- else:
2216
- module, _, klass = category.rpartition(".")
2217
- m = importlib.import_module(module)
2218
- cat = getattr(m, klass)
2219
- if not issubclass(cat, Warning):
2220
- raise UsageError(f"{cat} is not a Warning subclass")
2221
- return cast(type[Warning], cat)
2222
-
2223
-
2224
- def apply_warning_filters(
2225
- config_filters: Iterable[str], cmdline_filters: Iterable[str]
2226
- ) -> None:
2227
- """Applies pytest-configured filters to the warnings module"""
2228
- # Filters should have this precedence: cmdline options, config.
2229
- # Filters should be applied in the inverse order of precedence.
2230
- for arg in config_filters:
2231
- try:
2232
- warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
2233
- except ImportError as e:
2234
- warnings.warn(
2235
- f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning
2236
- )
2237
- continue
2238
-
2239
- for arg in cmdline_filters:
2240
- try:
2241
- warnings.filterwarnings(*parse_warning_filter(arg, escape=True))
2242
- except ImportError as e:
2243
- warnings.warn(
2244
- f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning
2245
- )
2246
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/config/argparsing.py DELETED
@@ -1,503 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- from __future__ import annotations
3
-
4
- import argparse
5
- from collections.abc import Callable
6
- from collections.abc import Sequence
7
- import os
8
- import sys
9
- import textwrap
10
- from typing import Any
11
- from typing import final
12
- from typing import Literal
13
- from typing import NoReturn
14
-
15
- from .exceptions import UsageError
16
- import _pytest._io
17
- from _pytest.compat import NOTSET
18
- from _pytest.deprecated import check_ispytest
19
-
20
-
21
- FILE_OR_DIR = "file_or_dir"
22
-
23
-
24
- @final
25
- class Parser:
26
- """Parser for command line arguments and config-file values.
27
-
28
- :ivar extra_info: Dict of generic param -> value to display in case
29
- there's an error processing the command line arguments.
30
- """
31
-
32
- def __init__(
33
- self,
34
- usage: str | None = None,
35
- processopt: Callable[[Argument], None] | None = None,
36
- *,
37
- prog: str | None = None,
38
- _ispytest: bool = False,
39
- ) -> None:
40
- check_ispytest(_ispytest)
41
-
42
- from _pytest._argcomplete import filescompleter
43
-
44
- self._processopt = processopt
45
- self.extra_info: dict[str, Any] = {}
46
- self.optparser = PytestArgumentParser(usage, self.extra_info, prog=prog)
47
- anonymous_arggroup = self.optparser.add_argument_group("Custom options")
48
- self._anonymous = OptionGroup(
49
- anonymous_arggroup, "_anonymous", self, _ispytest=True
50
- )
51
- self._groups = [self._anonymous]
52
- # Maps option strings -> dest, e.g. "-V" and "--version" to "version".
53
- self._opt2dest: dict[str, str] = {}
54
- file_or_dir_arg = self.optparser.add_argument(FILE_OR_DIR, nargs="*")
55
- file_or_dir_arg.completer = filescompleter # type: ignore
56
-
57
- self._inidict: dict[str, tuple[str, str, Any]] = {}
58
- # Maps alias -> canonical name.
59
- self._ini_aliases: dict[str, str] = {}
60
-
61
- @property
62
- def prog(self) -> str:
63
- return self.optparser.prog
64
-
65
- @prog.setter
66
- def prog(self, value: str) -> None:
67
- self.optparser.prog = value
68
-
69
- def processoption(self, option: Argument) -> None:
70
- if self._processopt:
71
- if option.dest:
72
- self._processopt(option)
73
-
74
- def getgroup(
75
- self, name: str, description: str = "", after: str | None = None
76
- ) -> OptionGroup:
77
- """Get (or create) a named option Group.
78
-
79
- :param name: Name of the option group.
80
- :param description: Long description for --help output.
81
- :param after: Name of another group, used for ordering --help output.
82
- :returns: The option group.
83
-
84
- The returned group object has an ``addoption`` method with the same
85
- signature as :func:`parser.addoption <pytest.Parser.addoption>` but
86
- will be shown in the respective group in the output of
87
- ``pytest --help``.
88
- """
89
- for group in self._groups:
90
- if group.name == name:
91
- return group
92
-
93
- arggroup = self.optparser.add_argument_group(description or name)
94
- group = OptionGroup(arggroup, name, self, _ispytest=True)
95
- i = 0
96
- for i, grp in enumerate(self._groups):
97
- if grp.name == after:
98
- break
99
- self._groups.insert(i + 1, group)
100
- # argparse doesn't provide a way to control `--help` order, so must
101
- # access its internals ☹.
102
- self.optparser._action_groups.insert(i + 1, self.optparser._action_groups.pop())
103
- return group
104
-
105
- def addoption(self, *opts: str, **attrs: Any) -> None:
106
- """Register a command line option.
107
-
108
- :param opts:
109
- Option names, can be short or long options.
110
- :param attrs:
111
- Same attributes as the argparse library's :meth:`add_argument()
112
- <argparse.ArgumentParser.add_argument>` function accepts.
113
-
114
- After command line parsing, options are available on the pytest config
115
- object via ``config.option.NAME`` where ``NAME`` is usually set
116
- by passing a ``dest`` attribute, for example
117
- ``addoption("--long", dest="NAME", ...)``.
118
- """
119
- self._anonymous.addoption(*opts, **attrs)
120
-
121
- def parse(
122
- self,
123
- args: Sequence[str | os.PathLike[str]],
124
- namespace: argparse.Namespace | None = None,
125
- ) -> argparse.Namespace:
126
- """Parse the arguments.
127
-
128
- Unlike ``parse_known_args`` and ``parse_known_and_unknown_args``,
129
- raises PrintHelp on `--help` and UsageError on unknown flags
130
-
131
- :meta private:
132
- """
133
- from _pytest._argcomplete import try_argcomplete
134
-
135
- try_argcomplete(self.optparser)
136
- strargs = [os.fspath(x) for x in args]
137
- if namespace is None:
138
- namespace = argparse.Namespace()
139
- try:
140
- namespace._raise_print_help = True
141
- return self.optparser.parse_intermixed_args(strargs, namespace=namespace)
142
- finally:
143
- del namespace._raise_print_help
144
-
145
- def parse_known_args(
146
- self,
147
- args: Sequence[str | os.PathLike[str]],
148
- namespace: argparse.Namespace | None = None,
149
- ) -> argparse.Namespace:
150
- """Parse the known arguments at this point.
151
-
152
- :returns: An argparse namespace object.
153
- """
154
- return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
155
-
156
- def parse_known_and_unknown_args(
157
- self,
158
- args: Sequence[str | os.PathLike[str]],
159
- namespace: argparse.Namespace | None = None,
160
- ) -> tuple[argparse.Namespace, list[str]]:
161
- """Parse the known arguments at this point, and also return the
162
- remaining unknown flag arguments.
163
-
164
- :returns:
165
- A tuple containing an argparse namespace object for the known
166
- arguments, and a list of unknown flag arguments.
167
- """
168
- strargs = [os.fspath(x) for x in args]
169
- if sys.version_info < (3, 12, 8) or (3, 13) <= sys.version_info < (3, 13, 1):
170
- # Older argparse have a bugged parse_known_intermixed_args.
171
- namespace, unknown = self.optparser.parse_known_args(strargs, namespace)
172
- assert namespace is not None
173
- file_or_dir = getattr(namespace, FILE_OR_DIR)
174
- unknown_flags: list[str] = []
175
- for arg in unknown:
176
- (unknown_flags if arg.startswith("-") else file_or_dir).append(arg)
177
- return namespace, unknown_flags
178
- else:
179
- return self.optparser.parse_known_intermixed_args(strargs, namespace)
180
-
181
- def addini(
182
- self,
183
- name: str,
184
- help: str,
185
- type: Literal[
186
- "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
187
- ]
188
- | None = None,
189
- default: Any = NOTSET,
190
- *,
191
- aliases: Sequence[str] = (),
192
- ) -> None:
193
- """Register a configuration file option.
194
-
195
- :param name:
196
- Name of the configuration.
197
- :param type:
198
- Type of the configuration. Can be:
199
-
200
- * ``string``: a string
201
- * ``bool``: a boolean
202
- * ``args``: a list of strings, separated as in a shell
203
- * ``linelist``: a list of strings, separated by line breaks
204
- * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
205
- * ``pathlist``: a list of ``py.path``, separated as in a shell
206
- * ``int``: an integer
207
- * ``float``: a floating-point number
208
-
209
- .. versionadded:: 8.4
210
-
211
- The ``float`` and ``int`` types.
212
-
213
- For ``paths`` and ``pathlist`` types, they are considered relative to the config-file.
214
- In case the execution is happening without a config-file defined,
215
- they will be considered relative to the current working directory (for example with ``--override-ini``).
216
-
217
- .. versionadded:: 7.0
218
- The ``paths`` variable type.
219
-
220
- .. versionadded:: 8.1
221
- Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of a config-file.
222
-
223
- Defaults to ``string`` if ``None`` or not passed.
224
- :param default:
225
- Default value if no config-file option exists but is queried.
226
- :param aliases:
227
- Additional names by which this option can be referenced.
228
- Aliases resolve to the canonical name.
229
-
230
- .. versionadded:: 9.0
231
- The ``aliases`` parameter.
232
-
233
- The value of configuration keys can be retrieved via a call to
234
- :py:func:`config.getini(name) <pytest.Config.getini>`.
235
- """
236
- assert type in (
237
- None,
238
- "string",
239
- "paths",
240
- "pathlist",
241
- "args",
242
- "linelist",
243
- "bool",
244
- "int",
245
- "float",
246
- )
247
- if type is None:
248
- type = "string"
249
- if default is NOTSET:
250
- default = get_ini_default_for_type(type)
251
-
252
- self._inidict[name] = (help, type, default)
253
-
254
- for alias in aliases:
255
- if alias in self._inidict:
256
- raise ValueError(
257
- f"alias {alias!r} conflicts with existing configuration option"
258
- )
259
- if (already := self._ini_aliases.get(alias)) is not None:
260
- raise ValueError(f"{alias!r} is already an alias of {already!r}")
261
- self._ini_aliases[alias] = name
262
-
263
-
264
- def get_ini_default_for_type(
265
- type: Literal[
266
- "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
267
- ],
268
- ) -> Any:
269
- """
270
- Used by addini to get the default value for a given config option type, when
271
- default is not supplied.
272
- """
273
- if type in ("paths", "pathlist", "args", "linelist"):
274
- return []
275
- elif type == "bool":
276
- return False
277
- elif type == "int":
278
- return 0
279
- elif type == "float":
280
- return 0.0
281
- else:
282
- return ""
283
-
284
-
285
- class Argument:
286
- """An option defined in an OptionGroup."""
287
-
288
- def __init__(self, action: argparse.Action) -> None:
289
- self._action = action
290
-
291
- def attrs(self) -> dict[str, Any]:
292
- return self._action.__dict__
293
-
294
- def names(self) -> Sequence[str]:
295
- return self._action.option_strings
296
-
297
- @property
298
- def dest(self) -> str:
299
- return self._action.dest
300
-
301
- @property
302
- def default(self) -> Any:
303
- return self._action.default
304
-
305
- @property
306
- def type(self) -> Any | None:
307
- return self._action.type
308
-
309
- def __repr__(self) -> str:
310
- action = getattr(self, "_action", None)
311
- if action is None:
312
- return "Argument(<uninitialized>)"
313
- args: list[str] = []
314
- args += ["opts: " + repr(self.names())]
315
- args += ["dest: " + repr(self.dest)]
316
- if action.type:
317
- args += ["type: " + repr(self.type)]
318
- args += ["default: " + repr(self.default)]
319
- return "Argument({})".format(", ".join(args))
320
-
321
-
322
- class OptionGroup:
323
- """A group of options shown in its own section."""
324
-
325
- def __init__(
326
- self,
327
- arggroup: argparse._ArgumentGroup,
328
- name: str,
329
- parser: Parser | None,
330
- _ispytest: bool = False,
331
- ) -> None:
332
- check_ispytest(_ispytest)
333
- self._arggroup = arggroup
334
- self.name = name
335
- self.options: list[Argument] = []
336
- self.parser = parser
337
-
338
- def addoption(self, *opts: str, **attrs: Any) -> None:
339
- """Add an option to this group.
340
-
341
- If a shortened version of a long option is specified, it will
342
- be suppressed in the help. ``addoption('--twowords', '--two-words')``
343
- results in help showing ``--two-words`` only, but ``--twowords`` gets
344
- accepted **and** the automatic destination is in ``args.twowords``.
345
-
346
- :param opts:
347
- Option names, can be short or long options.
348
- Note that lower-case short options (e.g. `-x`) are reserved.
349
- :param attrs:
350
- Same attributes as the argparse library's :meth:`add_argument()
351
- <argparse.ArgumentParser.add_argument>` function accepts.
352
- """
353
- conflict = set(opts).intersection(
354
- name for opt in self.options for name in opt.names()
355
- )
356
- if conflict:
357
- raise ValueError(f"option names {conflict} already added")
358
- self._addoption_inner(opts, attrs, allow_reserved=False)
359
-
360
- def _addoption(self, *opts: str, **attrs: Any) -> None:
361
- """Like addoption(), but also allows registering short lower case options (e.g. -x),
362
- which are reserved for pytest core."""
363
- self._addoption_inner(opts, attrs, allow_reserved=True)
364
-
365
- def _addoption_inner(
366
- self, opts: tuple[str, ...], attrs: dict[str, Any], allow_reserved: bool
367
- ) -> None:
368
- if not allow_reserved:
369
- for opt in opts:
370
- if len(opt) >= 2 and opt[0] == "-" and opt[1].islower():
371
- raise ValueError("lowercase short options are reserved")
372
-
373
- action = self._arggroup.add_argument(*opts, **attrs)
374
- option = Argument(action)
375
- self.options.append(option)
376
- if self.parser:
377
- for name in option.names():
378
- self.parser._opt2dest[name] = option.dest
379
- self.parser.processoption(option)
380
-
381
-
382
- class PytestArgumentParser(argparse.ArgumentParser):
383
- def __init__(
384
- self,
385
- usage: str | None,
386
- extra_info: dict[str, str],
387
- *,
388
- prog: str | None = None,
389
- ) -> None:
390
- super().__init__(
391
- usage=usage,
392
- prog=prog,
393
- add_help=False,
394
- formatter_class=DropShorterLongHelpFormatter,
395
- allow_abbrev=False,
396
- fromfile_prefix_chars="@",
397
- )
398
- # extra_info is a dict of (param -> value) to display if there's
399
- # an usage error to provide more contextual information to the user.
400
- self.extra_info = extra_info
401
-
402
- def error(self, message: str) -> NoReturn:
403
- """Transform argparse error message into UsageError."""
404
- # TODO(py313): Replace with `exit_on_error=False`. Note that while it
405
- # was added in Python 3.9, it was broken until 3.13 (cpython#121018).
406
- msg = f"{self.prog}: error: {message}"
407
- if self.extra_info:
408
- msg += "\n" + "\n".join(
409
- f" {k}: {v}" for k, v in sorted(self.extra_info.items())
410
- )
411
- raise UsageError(self.format_usage() + msg)
412
-
413
-
414
- class DropShorterLongHelpFormatter(argparse.HelpFormatter):
415
- """Shorten help for long options that differ only in extra hyphens.
416
-
417
- - Collapse **long** options that are the same except for extra hyphens.
418
- - Shortcut if there are only two options and one of them is a short one.
419
- """
420
-
421
- def __init__(self, *args: Any, **kwargs: Any) -> None:
422
- # Use more accurate terminal width.
423
- if "width" not in kwargs:
424
- kwargs["width"] = _pytest._io.get_terminal_width()
425
- super().__init__(*args, **kwargs)
426
-
427
- def _format_action_invocation(self, action: argparse.Action) -> str:
428
- orgstr = super()._format_action_invocation(action)
429
- if orgstr and orgstr[0] != "-": # only optional arguments
430
- return orgstr
431
- options = orgstr.split(", ")
432
- if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
433
- # a shortcut for '-h, --help' or '--abc', '-a'
434
- return orgstr
435
- return_list = []
436
- short_long: dict[str, str] = {}
437
- for option in options:
438
- if len(option) == 2 or option[2] == " ":
439
- continue
440
- assert option.startswith("--"), (
441
- f'long optional argument without "--": [{option}]'
442
- )
443
- xxoption = option[2:]
444
- shortened = xxoption.replace("-", "")
445
- if shortened not in short_long or len(short_long[shortened]) < len(
446
- xxoption
447
- ):
448
- short_long[shortened] = xxoption
449
- # now short_long has been filled out to the longest with dashes
450
- # **and** we keep the right option ordering from add_argument
451
- for option in options:
452
- if len(option) == 2 or option[2] == " ":
453
- return_list.append(option)
454
- if option[2:] == short_long.get(option.replace("-", "")):
455
- return_list.append(option.replace(" ", "=", 1))
456
- return ", ".join(return_list)
457
-
458
- def _split_lines(self, text: str, width: int) -> list[str]:
459
- """Wrap lines after splitting on original newlines.
460
-
461
- This allows to have explicit line breaks in the help text.
462
- """
463
- lines = []
464
- for line in text.splitlines():
465
- lines.extend(textwrap.wrap(line.strip(), width))
466
- return lines
467
-
468
-
469
- class OverrideIniAction(argparse.Action):
470
- """Custom argparse action that makes a CLI flag equivalent to overriding an
471
- option, in addition to behaving like `store_true`.
472
-
473
- This can simplify things since code only needs to inspect the config option
474
- and not consider the CLI flag.
475
- """
476
-
477
- def __init__(
478
- self,
479
- option_strings: Sequence[str],
480
- dest: str,
481
- nargs: int | str | None = None,
482
- *args,
483
- ini_option: str,
484
- ini_value: str,
485
- **kwargs,
486
- ) -> None:
487
- super().__init__(option_strings, dest, 0, *args, **kwargs)
488
- self.ini_option = ini_option
489
- self.ini_value = ini_value
490
-
491
- def __call__(
492
- self,
493
- parser: argparse.ArgumentParser,
494
- namespace: argparse.Namespace,
495
- *args,
496
- **kwargs,
497
- ) -> None:
498
- setattr(namespace, self.dest, True)
499
- current_overrides = getattr(namespace, "override_ini", None)
500
- if current_overrides is None:
501
- current_overrides = []
502
- current_overrides.append(f"{self.ini_option}={self.ini_value}")
503
- setattr(namespace, "override_ini", current_overrides)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/config/exceptions.py DELETED
@@ -1,15 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import final
4
-
5
-
6
- @final
7
- class UsageError(Exception):
8
- """Error in pytest usage or invocation."""
9
-
10
- __module__ = "pytest"
11
-
12
-
13
- class PrintHelp(Exception):
14
- """Raised when pytest should print its help to skip the rest of the
15
- argument parsing and validation."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/config/findpaths.py DELETED
@@ -1,350 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Iterable
4
- from collections.abc import Sequence
5
- from dataclasses import dataclass
6
- from dataclasses import KW_ONLY
7
- import os
8
- from pathlib import Path
9
- import sys
10
- from typing import Literal
11
- from typing import TypeAlias
12
-
13
- import iniconfig
14
-
15
- from .exceptions import UsageError
16
- from _pytest.outcomes import fail
17
- from _pytest.pathlib import absolutepath
18
- from _pytest.pathlib import commonpath
19
- from _pytest.pathlib import safe_exists
20
-
21
-
22
- @dataclass(frozen=True)
23
- class ConfigValue:
24
- """Represents a configuration value with its origin and parsing mode.
25
-
26
- This allows tracking whether a value came from a configuration file
27
- or from a CLI override (--override-ini), which is important for
28
- determining precedence when dealing with ini option aliases.
29
-
30
- The mode tracks the parsing mode/data model used for the value:
31
- - "ini": from INI files or [tool.pytest.ini_options], where the only
32
- supported value types are `str` or `list[str]`.
33
- - "toml": from TOML files (not in INI mode), where native TOML types
34
- are preserved.
35
- """
36
-
37
- value: object
38
- _: KW_ONLY
39
- origin: Literal["file", "override"]
40
- mode: Literal["ini", "toml"]
41
-
42
-
43
- ConfigDict: TypeAlias = dict[str, ConfigValue]
44
-
45
-
46
- def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
47
- """Parse the given generic '.ini' file using legacy IniConfig parser, returning
48
- the parsed object.
49
-
50
- Raise UsageError if the file cannot be parsed.
51
- """
52
- try:
53
- return iniconfig.IniConfig(str(path))
54
- except iniconfig.ParseError as exc:
55
- raise UsageError(str(exc)) from exc
56
-
57
-
58
- def load_config_dict_from_file(
59
- filepath: Path,
60
- ) -> ConfigDict | None:
61
- """Load pytest configuration from the given file path, if supported.
62
-
63
- Return None if the file does not contain valid pytest configuration.
64
- """
65
- # Configuration from ini files are obtained from the [pytest] section, if present.
66
- if filepath.suffix == ".ini":
67
- iniconfig = _parse_ini_config(filepath)
68
-
69
- if "pytest" in iniconfig:
70
- return {
71
- k: ConfigValue(v, origin="file", mode="ini")
72
- for k, v in iniconfig["pytest"].items()
73
- }
74
- else:
75
- # "pytest.ini" files are always the source of configuration, even if empty.
76
- if filepath.name in {"pytest.ini", ".pytest.ini"}:
77
- return {}
78
-
79
- # '.cfg' files are considered if they contain a "[tool:pytest]" section.
80
- elif filepath.suffix == ".cfg":
81
- iniconfig = _parse_ini_config(filepath)
82
-
83
- if "tool:pytest" in iniconfig.sections:
84
- return {
85
- k: ConfigValue(v, origin="file", mode="ini")
86
- for k, v in iniconfig["tool:pytest"].items()
87
- }
88
- elif "pytest" in iniconfig.sections:
89
- # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
90
- # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
91
- fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
92
-
93
- # '.toml' files are considered if they contain a [tool.pytest] table (toml mode)
94
- # or [tool.pytest.ini_options] table (ini mode) for pyproject.toml,
95
- # or [pytest] table (toml mode) for pytest.toml/.pytest.toml.
96
- elif filepath.suffix == ".toml":
97
- if sys.version_info >= (3, 11):
98
- import tomllib
99
- else:
100
- import tomli as tomllib
101
-
102
- toml_text = filepath.read_text(encoding="utf-8")
103
- try:
104
- config = tomllib.loads(toml_text)
105
- except tomllib.TOMLDecodeError as exc:
106
- raise UsageError(f"{filepath}: {exc}") from exc
107
-
108
- # pytest.toml and .pytest.toml use [pytest] table directly.
109
- if filepath.name in ("pytest.toml", ".pytest.toml"):
110
- pytest_config = config.get("pytest", {})
111
- if pytest_config:
112
- # TOML mode - preserve native TOML types.
113
- return {
114
- k: ConfigValue(v, origin="file", mode="toml")
115
- for k, v in pytest_config.items()
116
- }
117
- # "pytest.toml" files are always the source of configuration, even if empty.
118
- return {}
119
-
120
- # pyproject.toml uses [tool.pytest] or [tool.pytest.ini_options].
121
- else:
122
- tool_pytest = config.get("tool", {}).get("pytest", {})
123
-
124
- # Check for toml mode config: [tool.pytest] with content outside of ini_options.
125
- toml_config = {k: v for k, v in tool_pytest.items() if k != "ini_options"}
126
- # Check for ini mode config: [tool.pytest.ini_options].
127
- ini_config = tool_pytest.get("ini_options", None)
128
-
129
- if toml_config and ini_config:
130
- raise UsageError(
131
- f"{filepath}: Cannot use both [tool.pytest] (native TOML types) and "
132
- "[tool.pytest.ini_options] (string-based INI format) simultaneously. "
133
- "Please use [tool.pytest] with native TOML types (recommended) "
134
- "or [tool.pytest.ini_options] for backwards compatibility."
135
- )
136
-
137
- if toml_config:
138
- # TOML mode - preserve native TOML types.
139
- return {
140
- k: ConfigValue(v, origin="file", mode="toml")
141
- for k, v in toml_config.items()
142
- }
143
-
144
- elif ini_config is not None:
145
- # INI mode - TOML supports richer data types than INI files, but we need to
146
- # convert all scalar values to str for compatibility with the INI system.
147
- def make_scalar(v: object) -> str | list[str]:
148
- return v if isinstance(v, list) else str(v)
149
-
150
- return {
151
- k: ConfigValue(make_scalar(v), origin="file", mode="ini")
152
- for k, v in ini_config.items()
153
- }
154
-
155
- return None
156
-
157
-
158
- def locate_config(
159
- invocation_dir: Path,
160
- args: Iterable[Path],
161
- ) -> tuple[Path | None, Path | None, ConfigDict, Sequence[str]]:
162
- """Search in the list of arguments for a valid ini-file for pytest,
163
- and return a tuple of (rootdir, inifile, cfg-dict, ignored-config-files), where
164
- ignored-config-files is a list of config basenames found that contain
165
- pytest configuration but were ignored."""
166
- config_names = [
167
- "pytest.toml",
168
- ".pytest.toml",
169
- "pytest.ini",
170
- ".pytest.ini",
171
- "pyproject.toml",
172
- "tox.ini",
173
- "setup.cfg",
174
- ]
175
- args = [x for x in args if not str(x).startswith("-")]
176
- if not args:
177
- args = [invocation_dir]
178
- found_pyproject_toml: Path | None = None
179
- ignored_config_files: list[str] = []
180
-
181
- for arg in args:
182
- argpath = absolutepath(arg)
183
- for base in (argpath, *argpath.parents):
184
- for config_name in config_names:
185
- p = base / config_name
186
- if p.is_file():
187
- if p.name == "pyproject.toml" and found_pyproject_toml is None:
188
- found_pyproject_toml = p
189
- ini_config = load_config_dict_from_file(p)
190
- if ini_config is not None:
191
- index = config_names.index(config_name)
192
- for remainder in config_names[index + 1 :]:
193
- p2 = base / remainder
194
- if (
195
- p2.is_file()
196
- and load_config_dict_from_file(p2) is not None
197
- ):
198
- ignored_config_files.append(remainder)
199
- return base, p, ini_config, ignored_config_files
200
- if found_pyproject_toml is not None:
201
- return found_pyproject_toml.parent, found_pyproject_toml, {}, []
202
- return None, None, {}, []
203
-
204
-
205
- def get_common_ancestor(
206
- invocation_dir: Path,
207
- paths: Iterable[Path],
208
- ) -> Path:
209
- common_ancestor: Path | None = None
210
- for path in paths:
211
- if not path.exists():
212
- continue
213
- if common_ancestor is None:
214
- common_ancestor = path
215
- else:
216
- if common_ancestor in path.parents or path == common_ancestor:
217
- continue
218
- elif path in common_ancestor.parents:
219
- common_ancestor = path
220
- else:
221
- shared = commonpath(path, common_ancestor)
222
- if shared is not None:
223
- common_ancestor = shared
224
- if common_ancestor is None:
225
- common_ancestor = invocation_dir
226
- elif common_ancestor.is_file():
227
- common_ancestor = common_ancestor.parent
228
- return common_ancestor
229
-
230
-
231
- def get_dirs_from_args(args: Iterable[str]) -> list[Path]:
232
- def is_option(x: str) -> bool:
233
- return x.startswith("-")
234
-
235
- def get_file_part_from_node_id(x: str) -> str:
236
- return x.split("::", maxsplit=1)[0]
237
-
238
- def get_dir_from_path(path: Path) -> Path:
239
- if path.is_dir():
240
- return path
241
- return path.parent
242
-
243
- # These look like paths but may not exist
244
- possible_paths = (
245
- absolutepath(get_file_part_from_node_id(arg))
246
- for arg in args
247
- if not is_option(arg)
248
- )
249
-
250
- return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
251
-
252
-
253
- def parse_override_ini(override_ini: Sequence[str] | None) -> ConfigDict:
254
- """Parse the -o/--override-ini command line arguments and return the overrides.
255
-
256
- :raises UsageError:
257
- If one of the values is malformed.
258
- """
259
- overrides = {}
260
- # override_ini is a list of "ini=value" options.
261
- # Always use the last item if multiple values are set for same ini-name,
262
- # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
263
- for ini_config in override_ini or ():
264
- try:
265
- key, user_ini_value = ini_config.split("=", 1)
266
- except ValueError as e:
267
- raise UsageError(
268
- f"-o/--override-ini expects option=value style (got: {ini_config!r})."
269
- ) from e
270
- else:
271
- overrides[key] = ConfigValue(user_ini_value, origin="override", mode="ini")
272
- return overrides
273
-
274
-
275
- CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
276
-
277
-
278
- def determine_setup(
279
- *,
280
- inifile: str | None,
281
- override_ini: Sequence[str] | None,
282
- args: Sequence[str],
283
- rootdir_cmd_arg: str | None,
284
- invocation_dir: Path,
285
- ) -> tuple[Path, Path | None, ConfigDict, Sequence[str]]:
286
- """Determine the rootdir, inifile and ini configuration values from the
287
- command line arguments.
288
-
289
- :param inifile:
290
- The `--inifile` command line argument, if given.
291
- :param override_ini:
292
- The -o/--override-ini command line arguments, if given.
293
- :param args:
294
- The free command line arguments.
295
- :param rootdir_cmd_arg:
296
- The `--rootdir` command line argument, if given.
297
- :param invocation_dir:
298
- The working directory when pytest was invoked.
299
-
300
- :raises UsageError:
301
- """
302
- rootdir = None
303
- dirs = get_dirs_from_args(args)
304
- ignored_config_files: Sequence[str] = []
305
-
306
- if inifile:
307
- inipath_ = absolutepath(inifile)
308
- inipath: Path | None = inipath_
309
- inicfg = load_config_dict_from_file(inipath_) or {}
310
- if rootdir_cmd_arg is None:
311
- rootdir = inipath_.parent
312
- else:
313
- ancestor = get_common_ancestor(invocation_dir, dirs)
314
- rootdir, inipath, inicfg, ignored_config_files = locate_config(
315
- invocation_dir, [ancestor]
316
- )
317
- if rootdir is None and rootdir_cmd_arg is None:
318
- for possible_rootdir in (ancestor, *ancestor.parents):
319
- if (possible_rootdir / "setup.py").is_file():
320
- rootdir = possible_rootdir
321
- break
322
- else:
323
- if dirs != [ancestor]:
324
- rootdir, inipath, inicfg, _ = locate_config(invocation_dir, dirs)
325
- if rootdir is None:
326
- rootdir = get_common_ancestor(
327
- invocation_dir, [invocation_dir, ancestor]
328
- )
329
- if is_fs_root(rootdir):
330
- rootdir = ancestor
331
- if rootdir_cmd_arg:
332
- rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
333
- if not rootdir.is_dir():
334
- raise UsageError(
335
- f"Directory '{rootdir}' not found. Check your '--rootdir' option."
336
- )
337
-
338
- ini_overrides = parse_override_ini(override_ini)
339
- inicfg.update(ini_overrides)
340
-
341
- assert rootdir is not None
342
- return rootdir, inipath, inicfg, ignored_config_files
343
-
344
-
345
- def is_fs_root(p: Path) -> bool:
346
- r"""
347
- Return True if the given path is pointing to the root of the
348
- file system ("/" on Unix and "C:\\" on Windows for example).
349
- """
350
- return os.path.splitdrive(str(p))[1] == os.sep
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/debugging.py DELETED
@@ -1,404 +0,0 @@
1
- # mypy: allow-untyped-defs
2
- # ruff: noqa: T100
3
- """Interactive debugging with PDB, the Python Debugger."""
4
-
5
- from __future__ import annotations
6
-
7
- import argparse
8
- from collections.abc import Callable
9
- from collections.abc import Generator
10
- import functools
11
- import importlib
12
- import sys
13
- import types
14
- from typing import Any
15
-
16
- from _pytest import outcomes
17
- from _pytest._code import ExceptionInfo
18
- from _pytest.capture import CaptureManager
19
- from _pytest.config import Config
20
- from _pytest.config import ConftestImportFailure
21
- from _pytest.config import hookimpl
22
- from _pytest.config import PytestPluginManager
23
- from _pytest.config.argparsing import Parser
24
- from _pytest.config.exceptions import UsageError
25
- from _pytest.nodes import Node
26
- from _pytest.reports import BaseReport
27
- from _pytest.runner import CallInfo
28
-
29
-
30
- def _validate_usepdb_cls(value: str) -> tuple[str, str]:
31
- """Validate syntax of --pdbcls option."""
32
- try:
33
- modname, classname = value.split(":")
34
- except ValueError as e:
35
- raise argparse.ArgumentTypeError(
36
- f"{value!r} is not in the format 'modname:classname'"
37
- ) from e
38
- return (modname, classname)
39
-
40
-
41
- def pytest_addoption(parser: Parser) -> None:
42
- group = parser.getgroup("general")
43
- group.addoption(
44
- "--pdb",
45
- dest="usepdb",
46
- action="store_true",
47
- help="Start the interactive Python debugger on errors or KeyboardInterrupt",
48
- )
49
- group.addoption(
50
- "--pdbcls",
51
- dest="usepdb_cls",
52
- metavar="modulename:classname",
53
- type=_validate_usepdb_cls,
54
- help="Specify a custom interactive Python debugger for use with --pdb."
55
- "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
56
- )
57
- group.addoption(
58
- "--trace",
59
- dest="trace",
60
- action="store_true",
61
- help="Immediately break when running each test",
62
- )
63
-
64
-
65
- def pytest_configure(config: Config) -> None:
66
- import pdb
67
-
68
- if config.getvalue("trace"):
69
- config.pluginmanager.register(PdbTrace(), "pdbtrace")
70
- if config.getvalue("usepdb"):
71
- config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
72
-
73
- pytestPDB._saved.append(
74
- (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config)
75
- )
76
- pdb.set_trace = pytestPDB.set_trace
77
- pytestPDB._pluginmanager = config.pluginmanager
78
- pytestPDB._config = config
79
-
80
- # NOTE: not using pytest_unconfigure, since it might get called although
81
- # pytest_configure was not (if another plugin raises UsageError).
82
- def fin() -> None:
83
- (
84
- pdb.set_trace,
85
- pytestPDB._pluginmanager,
86
- pytestPDB._config,
87
- ) = pytestPDB._saved.pop()
88
-
89
- config.add_cleanup(fin)
90
-
91
-
92
- class pytestPDB:
93
- """Pseudo PDB that defers to the real pdb."""
94
-
95
- _pluginmanager: PytestPluginManager | None = None
96
- _config: Config | None = None
97
- _saved: list[
98
- tuple[Callable[..., None], PytestPluginManager | None, Config | None]
99
- ] = []
100
- _recursive_debug = 0
101
- _wrapped_pdb_cls: tuple[type[Any], type[Any]] | None = None
102
-
103
- @classmethod
104
- def _is_capturing(cls, capman: CaptureManager | None) -> str | bool:
105
- if capman:
106
- return capman.is_capturing()
107
- return False
108
-
109
- @classmethod
110
- def _import_pdb_cls(cls, capman: CaptureManager | None):
111
- if not cls._config:
112
- import pdb
113
-
114
- # Happens when using pytest.set_trace outside of a test.
115
- return pdb.Pdb
116
-
117
- usepdb_cls = cls._config.getvalue("usepdb_cls")
118
-
119
- if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
120
- return cls._wrapped_pdb_cls[1]
121
-
122
- if usepdb_cls:
123
- modname, classname = usepdb_cls
124
-
125
- try:
126
- mod = importlib.import_module(modname)
127
-
128
- # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
129
- parts = classname.split(".")
130
- pdb_cls = getattr(mod, parts[0])
131
- for part in parts[1:]:
132
- pdb_cls = getattr(pdb_cls, part)
133
- except Exception as exc:
134
- value = ":".join((modname, classname))
135
- raise UsageError(
136
- f"--pdbcls: could not import {value!r}: {exc}"
137
- ) from exc
138
- else:
139
- import pdb
140
-
141
- pdb_cls = pdb.Pdb
142
-
143
- wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
144
- cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
145
- return wrapped_cls
146
-
147
- @classmethod
148
- def _get_pdb_wrapper_class(cls, pdb_cls, capman: CaptureManager | None):
149
- import _pytest.config
150
-
151
- class PytestPdbWrapper(pdb_cls):
152
- _pytest_capman = capman
153
- _continued = False
154
-
155
- def do_debug(self, arg):
156
- cls._recursive_debug += 1
157
- ret = super().do_debug(arg)
158
- cls._recursive_debug -= 1
159
- return ret
160
-
161
- if hasattr(pdb_cls, "do_debug"):
162
- do_debug.__doc__ = pdb_cls.do_debug.__doc__
163
-
164
- def do_continue(self, arg):
165
- ret = super().do_continue(arg)
166
- if cls._recursive_debug == 0:
167
- assert cls._config is not None
168
- tw = _pytest.config.create_terminal_writer(cls._config)
169
- tw.line()
170
-
171
- capman = self._pytest_capman
172
- capturing = pytestPDB._is_capturing(capman)
173
- if capturing:
174
- if capturing == "global":
175
- tw.sep(">", "PDB continue (IO-capturing resumed)")
176
- else:
177
- tw.sep(
178
- ">",
179
- f"PDB continue (IO-capturing resumed for {capturing})",
180
- )
181
- assert capman is not None
182
- capman.resume()
183
- else:
184
- tw.sep(">", "PDB continue")
185
- assert cls._pluginmanager is not None
186
- cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
187
- self._continued = True
188
- return ret
189
-
190
- if hasattr(pdb_cls, "do_continue"):
191
- do_continue.__doc__ = pdb_cls.do_continue.__doc__
192
-
193
- do_c = do_cont = do_continue
194
-
195
- def do_quit(self, arg):
196
- # Raise Exit outcome when quit command is used in pdb.
197
- #
198
- # This is a bit of a hack - it would be better if BdbQuit
199
- # could be handled, but this would require to wrap the
200
- # whole pytest run, and adjust the report etc.
201
- ret = super().do_quit(arg)
202
-
203
- if cls._recursive_debug == 0:
204
- outcomes.exit("Quitting debugger")
205
-
206
- return ret
207
-
208
- if hasattr(pdb_cls, "do_quit"):
209
- do_quit.__doc__ = pdb_cls.do_quit.__doc__
210
-
211
- do_q = do_quit
212
- do_exit = do_quit
213
-
214
- def setup(self, f, tb):
215
- """Suspend on setup().
216
-
217
- Needed after do_continue resumed, and entering another
218
- breakpoint again.
219
- """
220
- ret = super().setup(f, tb)
221
- if not ret and self._continued:
222
- # pdb.setup() returns True if the command wants to exit
223
- # from the interaction: do not suspend capturing then.
224
- if self._pytest_capman:
225
- self._pytest_capman.suspend_global_capture(in_=True)
226
- return ret
227
-
228
- def get_stack(self, f, t):
229
- stack, i = super().get_stack(f, t)
230
- if f is None:
231
- # Find last non-hidden frame.
232
- i = max(0, len(stack) - 1)
233
- while i and stack[i][0].f_locals.get("__tracebackhide__", False):
234
- i -= 1
235
- return stack, i
236
-
237
- return PytestPdbWrapper
238
-
239
- @classmethod
240
- def _init_pdb(cls, method, *args, **kwargs):
241
- """Initialize PDB debugging, dropping any IO capturing."""
242
- import _pytest.config
243
-
244
- if cls._pluginmanager is None:
245
- capman: CaptureManager | None = None
246
- else:
247
- capman = cls._pluginmanager.getplugin("capturemanager")
248
- if capman:
249
- capman.suspend(in_=True)
250
-
251
- if cls._config:
252
- tw = _pytest.config.create_terminal_writer(cls._config)
253
- tw.line()
254
-
255
- if cls._recursive_debug == 0:
256
- # Handle header similar to pdb.set_trace in py37+.
257
- header = kwargs.pop("header", None)
258
- if header is not None:
259
- tw.sep(">", header)
260
- else:
261
- capturing = cls._is_capturing(capman)
262
- if capturing == "global":
263
- tw.sep(">", f"PDB {method} (IO-capturing turned off)")
264
- elif capturing:
265
- tw.sep(
266
- ">",
267
- f"PDB {method} (IO-capturing turned off for {capturing})",
268
- )
269
- else:
270
- tw.sep(">", f"PDB {method}")
271
-
272
- _pdb = cls._import_pdb_cls(capman)(**kwargs)
273
-
274
- if cls._pluginmanager:
275
- cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
276
- return _pdb
277
-
278
- @classmethod
279
- def set_trace(cls, *args, **kwargs) -> None:
280
- """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
281
- frame = sys._getframe().f_back
282
- _pdb = cls._init_pdb("set_trace", *args, **kwargs)
283
- _pdb.set_trace(frame)
284
-
285
-
286
- class PdbInvoke:
287
- def pytest_exception_interact(
288
- self, node: Node, call: CallInfo[Any], report: BaseReport
289
- ) -> None:
290
- capman = node.config.pluginmanager.getplugin("capturemanager")
291
- if capman:
292
- capman.suspend_global_capture(in_=True)
293
- out, err = capman.read_global_capture()
294
- sys.stdout.write(out)
295
- sys.stdout.write(err)
296
- assert call.excinfo is not None
297
- _enter_pdb(node, call.excinfo, report)
298
-
299
- def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
300
- exc_or_tb = _postmortem_exc_or_tb(excinfo)
301
- post_mortem(exc_or_tb)
302
-
303
-
304
- class PdbTrace:
305
- @hookimpl(wrapper=True)
306
- def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
307
- wrap_pytest_function_for_tracing(pyfuncitem)
308
- return (yield)
309
-
310
-
311
- def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
312
- """Change the Python function object of the given Function item by a
313
- wrapper which actually enters pdb before calling the python function
314
- itself, effectively leaving the user in the pdb prompt in the first
315
- statement of the function."""
316
- _pdb = pytestPDB._init_pdb("runcall")
317
- testfunction = pyfuncitem.obj
318
-
319
- # we can't just return `partial(pdb.runcall, testfunction)` because (on
320
- # python < 3.7.4) runcall's first param is `func`, which means we'd get
321
- # an exception if one of the kwargs to testfunction was called `func`.
322
- @functools.wraps(testfunction)
323
- def wrapper(*args, **kwargs) -> None:
324
- func = functools.partial(testfunction, *args, **kwargs)
325
- _pdb.runcall(func)
326
-
327
- pyfuncitem.obj = wrapper
328
-
329
-
330
- def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None:
331
- """Wrap the given pytestfunct item for tracing support if --trace was given in
332
- the command line."""
333
- if pyfuncitem.config.getvalue("trace"):
334
- wrap_pytest_function_for_tracing(pyfuncitem)
335
-
336
-
337
- def _enter_pdb(
338
- node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport
339
- ) -> BaseReport:
340
- # XXX we reuse the TerminalReporter's terminalwriter
341
- # because this seems to avoid some encoding related troubles
342
- # for not completely clear reasons.
343
- tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
344
- tw.line()
345
-
346
- showcapture = node.config.option.showcapture
347
-
348
- for sectionname, content in (
349
- ("stdout", rep.capstdout),
350
- ("stderr", rep.capstderr),
351
- ("log", rep.caplog),
352
- ):
353
- if showcapture in (sectionname, "all") and content:
354
- tw.sep(">", "captured " + sectionname)
355
- if content[-1:] == "\n":
356
- content = content[:-1]
357
- tw.line(content)
358
-
359
- tw.sep(">", "traceback")
360
- rep.toterminal(tw)
361
- tw.sep(">", "entering PDB")
362
- tb_or_exc = _postmortem_exc_or_tb(excinfo)
363
- rep._pdbshown = True # type: ignore[attr-defined]
364
- post_mortem(tb_or_exc)
365
- return rep
366
-
367
-
368
- def _postmortem_exc_or_tb(
369
- excinfo: ExceptionInfo[BaseException],
370
- ) -> types.TracebackType | BaseException:
371
- from doctest import UnexpectedException
372
-
373
- get_exc = sys.version_info >= (3, 13)
374
- if isinstance(excinfo.value, UnexpectedException):
375
- # A doctest.UnexpectedException is not useful for post_mortem.
376
- # Use the underlying exception instead:
377
- underlying_exc = excinfo.value
378
- if get_exc:
379
- return underlying_exc.exc_info[1]
380
-
381
- return underlying_exc.exc_info[2]
382
- elif isinstance(excinfo.value, ConftestImportFailure):
383
- # A config.ConftestImportFailure is not useful for post_mortem.
384
- # Use the underlying exception instead:
385
- cause = excinfo.value.cause
386
- if get_exc:
387
- return cause
388
-
389
- assert cause.__traceback__ is not None
390
- return cause.__traceback__
391
- else:
392
- assert excinfo._excinfo is not None
393
- if get_exc:
394
- return excinfo._excinfo[1]
395
-
396
- return excinfo._excinfo[2]
397
-
398
-
399
- def post_mortem(tb_or_exc: types.TracebackType | BaseException) -> None:
400
- p = pytestPDB._init_pdb("post_mortem")
401
- p.reset()
402
- p.interaction(None, tb_or_exc)
403
- if p.quitting:
404
- outcomes.exit("Quitting debugger")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
venv/Lib/site-packages/_pytest/deprecated.py DELETED
@@ -1,139 +0,0 @@
1
- """Deprecation messages and bits of code used elsewhere in the codebase that
2
- is planned to be removed in the next pytest release.
3
-
4
- Keeping it in a central location makes it easy to track what is deprecated and should
5
- be removed when the time comes.
6
-
7
- All constants defined in this module should be either instances of
8
- :class:`PytestWarning`, or :class:`UnformattedWarning`
9
- in case of warnings which need to format their messages.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- from warnings import warn
15
-
16
- from _pytest.warning_types import PytestDeprecationWarning
17
- from _pytest.warning_types import PytestRemovedIn10Warning
18
- from _pytest.warning_types import UnformattedWarning
19
-
20
-
21
- # set of plugins which have been integrated into the core; we use this list to ignore
22
- # them during registration to avoid conflicts
23
- DEPRECATED_EXTERNAL_PLUGINS = {
24
- "pytest_catchlog",
25
- "pytest_capturelog",
26
- "pytest_faulthandler",
27
- "pytest_subtests",
28
- }
29
-
30
-
31
- # This could have been removed pytest 8, but it's harmless and common, so no rush to remove.
32
- YIELD_FIXTURE = PytestDeprecationWarning(
33
- "@pytest.yield_fixture is deprecated.\n"
34
- "Use @pytest.fixture instead; they are the same."
35
- )
36
-
37
- CLASS_FIXTURE_INSTANCE_METHOD = PytestRemovedIn10Warning(
38
- "Class-scoped fixture defined as instance method is deprecated.\n"
39
- "Instance attributes set in this fixture will NOT be visible to test methods,\n"
40
- "as each test gets a new instance while the fixture runs only once per class.\n"
41
- "Use @classmethod decorator and set attributes on cls instead.\n"
42
- "See https://docs.pytest.org/en/stable/deprecations.html#class-scoped-fixture-as-instance-method"
43
- )
44
-
45
- # This deprecation is never really meant to be removed.
46
- PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
47
-
48
-
49
- HOOK_LEGACY_MARKING = UnformattedWarning(
50
- PytestRemovedIn10Warning,
51
- "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
52
- "Please use the pytest.hook{type}({hook_opts}) decorator instead\n"
53
- " to configure the hooks.\n"
54
- " See https://docs.pytest.org/en/latest/deprecations.html"
55
- "#configuring-hook-specs-impls-using-markers",
56
- )
57
-
58
- MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES = PytestRemovedIn10Warning(
59
- "monkeypatch.syspath_prepend() called with pkg_resources legacy namespace packages detected.\n"
60
- "Legacy namespace packages (using pkg_resources.declare_namespace) are deprecated.\n"
61
- "Please use native namespace packages (PEP 420) instead.\n"
62
- "See https://docs.pytest.org/en/stable/deprecations.html#monkeypatch-fixup-namespace-packages"
63
- )
64
-
65
- PARAMETRIZE_NON_COLLECTION_ITERABLE = UnformattedWarning(
66
- PytestRemovedIn10Warning,
67
- "Passing a non-Collection iterable to parametrize is deprecated.\n"
68
- "Test: {nodeid}, argvalues type: {type_name}\n"
69
- "Please convert to a list or tuple.\n"
70
- "See https://docs.pytest.org/en/stable/deprecations.html#parametrize-iterators",
71
- )
72
-
73
- CONSOLE_MAIN = PytestRemovedIn10Warning(
74
- "pytest.console_main() is deprecated and will be removed in pytest 10.\n"
75
- "It was never intended for programmatic use; use pytest.main() instead.\n"
76
- "See https://docs.pytest.org/en/stable/deprecations.html#console-main"
77
- )
78
-
79
- CONFIG_INICFG = PytestRemovedIn10Warning(
80
- "config.inicfg is deprecated, use config.getini() to access configuration values instead.\n"
81
- "See https://docs.pytest.org/en/stable/deprecations.html#config-inicfg"
82
- )
83
-
84
- FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN = UnformattedWarning(
85
- PytestRemovedIn10Warning,
86
- 'Calling request.getfixturevalue("{argname}") during teardown is deprecated.\n'
87
- "Please request the fixture before teardown begins, either by declaring it in the fixture signature "
88
- "or by calling request.getfixturevalue() before the fixture yields.\n"
89
- "See https://docs.pytest.org/en/stable/deprecations.html#dynamic-fixture-request-during-teardown",
90
- )
91
-
92
- PASTEBIN = PytestRemovedIn10Warning(
93
- "The --pastebin option is deprecated. "
94
- "The functionality is now available in an external plugin package, pytest-pastebin.\n"
95
- "See https://docs.pytest.org/en/stable/deprecations.html#the-pastebin-option"
96
- )
97
-
98
- # You want to make some `__init__` or function "private".
99
- #
100
- # def my_private_function(some, args):
101
- # ...
102
- #
103
- # Do this:
104
- #
105
- # def my_private_function(some, args, *, _ispytest: bool = False):
106
- # check_ispytest(_ispytest)
107
- # ...
108
- #
109
- # Change all internal/allowed calls to
110
- #
111
- # my_private_function(some, args, _ispytest=True)
112
- #
113
- # All other calls will get the default _ispytest=False and trigger
114
- # the warning (possibly error in the future).
115
-
116
-
117
- FIXTURE_BASEID_DEPRECATED = PytestRemovedIn10Warning(
118
- "Passing baseid to FixtureDef is deprecated. Pass node instead for fixture scoping."
119
- )
120
-
121
- FIXTURE_NODEID_DEPRECATED = PytestRemovedIn10Warning(
122
- "Passing nodeid to _register_fixture is deprecated. "
123
- "Pass node instead for fixture scoping."
124
- )
125
-
126
- FIXTUREDEF_HAS_LOCATION_DEPRECATED = PytestRemovedIn10Warning(
127
- "FixtureDef.has_location is deprecated and will be removed in pytest 10. "
128
- "See https://docs.pytest.org/en/stable/deprecations.html#fixturedef-has-location-deprecated"
129
- )
130
-
131
- PARSEFACTORIES_NODEID_DEPRECATED = PytestRemovedIn10Warning(
132
- "Passing nodeid string to parsefactories is deprecated. "
133
- "Use parsefactories(holder=obj, node=node) instead."
134
- )
135
-
136
-
137
- def check_ispytest(ispytest: bool) -> None:
138
- if not ispytest:
139
- warn(PRIVATE, stacklevel=3)