cleanup: remove venv from image repo
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- venv/.gitignore +0 -2
- venv/Lib/site-packages/PySocks-1.7.1.dist-info/INSTALLER +0 -1
- venv/Lib/site-packages/PySocks-1.7.1.dist-info/LICENSE +0 -22
- venv/Lib/site-packages/PySocks-1.7.1.dist-info/METADATA +0 -321
- venv/Lib/site-packages/PySocks-1.7.1.dist-info/RECORD +0 -11
- venv/Lib/site-packages/PySocks-1.7.1.dist-info/REQUESTED +0 -0
- venv/Lib/site-packages/PySocks-1.7.1.dist-info/WHEEL +0 -5
- venv/Lib/site-packages/PySocks-1.7.1.dist-info/top_level.txt +0 -2
- venv/Lib/site-packages/StrEnum-0.4.15.dist-info/INSTALLER +0 -1
- venv/Lib/site-packages/StrEnum-0.4.15.dist-info/LICENSE +0 -21
- venv/Lib/site-packages/StrEnum-0.4.15.dist-info/METADATA +0 -211
- venv/Lib/site-packages/StrEnum-0.4.15.dist-info/RECORD +0 -18
- venv/Lib/site-packages/StrEnum-0.4.15.dist-info/WHEEL +0 -5
- venv/Lib/site-packages/StrEnum-0.4.15.dist-info/top_level.txt +0 -1
- venv/Lib/site-packages/_cffi_backend.cp314-win_amd64.pyd +0 -3
- venv/Lib/site-packages/_pytest/__init__.py +0 -13
- venv/Lib/site-packages/_pytest/_argcomplete.py +0 -117
- venv/Lib/site-packages/_pytest/_code/__init__.py +0 -26
- venv/Lib/site-packages/_pytest/_code/code.py +0 -1632
- venv/Lib/site-packages/_pytest/_code/source.py +0 -228
- venv/Lib/site-packages/_pytest/_io/__init__.py +0 -10
- venv/Lib/site-packages/_pytest/_io/pprint.py +0 -673
- venv/Lib/site-packages/_pytest/_io/saferepr.py +0 -155
- venv/Lib/site-packages/_pytest/_io/terminalwriter.py +0 -258
- venv/Lib/site-packages/_pytest/_io/wcwidth.py +0 -57
- venv/Lib/site-packages/_pytest/_py/__init__.py +0 -0
- venv/Lib/site-packages/_pytest/_py/error.py +0 -119
- venv/Lib/site-packages/_pytest/_py/path.py +0 -1475
- venv/Lib/site-packages/_pytest/_version.py +0 -24
- venv/Lib/site-packages/_pytest/assertion/__init__.py +0 -236
- venv/Lib/site-packages/_pytest/assertion/_compare_any.py +0 -131
- venv/Lib/site-packages/_pytest/assertion/_compare_mapping.py +0 -48
- venv/Lib/site-packages/_pytest/assertion/_compare_sequence.py +0 -90
- venv/Lib/site-packages/_pytest/assertion/_compare_set.py +0 -101
- venv/Lib/site-packages/_pytest/assertion/_guards.py +0 -60
- venv/Lib/site-packages/_pytest/assertion/_typing.py +0 -12
- venv/Lib/site-packages/_pytest/assertion/compare_text.py +0 -104
- venv/Lib/site-packages/_pytest/assertion/highlight.py +0 -11
- venv/Lib/site-packages/_pytest/assertion/rewrite.py +0 -1193
- venv/Lib/site-packages/_pytest/assertion/truncate.py +0 -128
- venv/Lib/site-packages/_pytest/assertion/util.py +0 -215
- venv/Lib/site-packages/_pytest/cacheprovider.py +0 -640
- venv/Lib/site-packages/_pytest/capture.py +0 -1151
- venv/Lib/site-packages/_pytest/compat.py +0 -329
- venv/Lib/site-packages/_pytest/config/__init__.py +0 -2246
- venv/Lib/site-packages/_pytest/config/argparsing.py +0 -503
- venv/Lib/site-packages/_pytest/config/exceptions.py +0 -15
- venv/Lib/site-packages/_pytest/config/findpaths.py +0 -350
- venv/Lib/site-packages/_pytest/debugging.py +0 -404
- 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 |
-
[](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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|