6
6
import enum
7
7
import inspect
8
8
import os
9
+ import re
9
10
import shlex
10
11
import sys
11
12
import types
15
16
from typing import Any
16
17
from typing import Callable
17
18
from typing import Dict
19
+ from typing import Generator
18
20
from typing import IO
19
21
from typing import Iterable
20
22
from typing import Iterator
@@ -342,6 +344,13 @@ def __init__(self) -> None:
342
344
self ._noconftest = False
343
345
self ._duplicatepaths = set () # type: Set[py.path.local]
344
346
347
+ # plugins that were explicitly skipped with pytest.skip
348
+ # list of (module name, skip reason)
349
+ # previously we would issue a warning when a plugin was skipped, but
350
+ # since we refactored warnings as first citizens of Config, they are
351
+ # just stored here to be used later.
352
+ self .skipped_plugins = [] # type: List[Tuple[str, str]]
353
+
345
354
self .add_hookspecs (_pytest .hookspec )
346
355
self .register (self )
347
356
if os .environ .get ("PYTEST_DEBUG" ):
@@ -694,13 +703,7 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No
694
703
).with_traceback (e .__traceback__ ) from e
695
704
696
705
except Skipped as e :
697
- from _pytest .warnings import _issue_warning_captured
698
-
699
- _issue_warning_captured (
700
- PytestConfigWarning ("skipped plugin {!r}: {}" .format (modname , e .msg )),
701
- self .hook ,
702
- stacklevel = 2 ,
703
- )
706
+ self .skipped_plugins .append ((modname , e .msg or "" ))
704
707
else :
705
708
mod = sys .modules [importspec ]
706
709
self .register (mod , modname )
@@ -1092,6 +1095,9 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None:
1092
1095
self ._validate_args (self .getini ("addopts" ), "via addopts config" ) + args
1093
1096
)
1094
1097
1098
+ self .known_args_namespace = self ._parser .parse_known_args (
1099
+ args , namespace = copy .copy (self .option )
1100
+ )
1095
1101
self ._checkversion ()
1096
1102
self ._consider_importhook (args )
1097
1103
self .pluginmanager .consider_preparse (args , exclude_only = False )
@@ -1100,10 +1106,10 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None:
1100
1106
# plugins are going to be loaded.
1101
1107
self .pluginmanager .load_setuptools_entrypoints ("pytest11" )
1102
1108
self .pluginmanager .consider_env ()
1103
- self .known_args_namespace = ns = self ._parser .parse_known_args (
1104
- args , namespace = copy .copy (self .option )
1105
- )
1109
+
1106
1110
self ._validate_plugins ()
1111
+ self ._warn_about_skipped_plugins ()
1112
+
1107
1113
if self .known_args_namespace .confcutdir is None and self .inifile :
1108
1114
confcutdir = py .path .local (self .inifile ).dirname
1109
1115
self .known_args_namespace .confcutdir = confcutdir
@@ -1112,21 +1118,24 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None:
1112
1118
early_config = self , args = args , parser = self ._parser
1113
1119
)
1114
1120
except ConftestImportFailure as e :
1115
- if ns . help or ns .version :
1121
+ if self . known_args_namespace . help or self . known_args_namespace .version :
1116
1122
# we don't want to prevent --help/--version to work
1117
1123
# so just let is pass and print a warning at the end
1118
- from _pytest .warnings import _issue_warning_captured
1119
-
1120
- _issue_warning_captured (
1124
+ self .issue_config_time_warning (
1121
1125
PytestConfigWarning (
1122
1126
"could not load initial conftests: {}" .format (e .path )
1123
1127
),
1124
- self .hook ,
1125
1128
stacklevel = 2 ,
1126
1129
)
1127
1130
else :
1128
1131
raise
1129
- self ._validate_keys ()
1132
+
1133
+ @hookimpl (hookwrapper = True )
1134
+ def pytest_collection (self ) -> Generator [None , None , None ]:
1135
+ """Validate invalid ini keys after collection is done so we take in account
1136
+ options added by late-loading conftest files."""
1137
+ yield
1138
+ self ._validate_config_options ()
1130
1139
1131
1140
def _checkversion (self ) -> None :
1132
1141
import pytest
@@ -1147,9 +1156,9 @@ def _checkversion(self) -> None:
1147
1156
% (self .inifile , minver , pytest .__version__ ,)
1148
1157
)
1149
1158
1150
- def _validate_keys (self ) -> None :
1159
+ def _validate_config_options (self ) -> None :
1151
1160
for key in sorted (self ._get_unknown_ini_keys ()):
1152
- self ._warn_or_fail_if_strict ("Unknown config ini key : {}\n " .format (key ))
1161
+ self ._warn_or_fail_if_strict ("Unknown config option : {}\n " .format (key ))
1153
1162
1154
1163
def _validate_plugins (self ) -> None :
1155
1164
required_plugins = sorted (self .getini ("required_plugins" ))
@@ -1165,7 +1174,6 @@ def _validate_plugins(self) -> None:
1165
1174
1166
1175
missing_plugins = []
1167
1176
for required_plugin in required_plugins :
1168
- spec = None
1169
1177
try :
1170
1178
spec = Requirement (required_plugin )
1171
1179
except InvalidRequirement :
@@ -1187,11 +1195,7 @@ def _warn_or_fail_if_strict(self, message: str) -> None:
1187
1195
if self .known_args_namespace .strict_config :
1188
1196
fail (message , pytrace = False )
1189
1197
1190
- from _pytest .warnings import _issue_warning_captured
1191
-
1192
- _issue_warning_captured (
1193
- PytestConfigWarning (message ), self .hook , stacklevel = 3 ,
1194
- )
1198
+ self .issue_config_time_warning (PytestConfigWarning (message ), stacklevel = 3 )
1195
1199
1196
1200
def _get_unknown_ini_keys (self ) -> List [str ]:
1197
1201
parser_inicfg = self ._parser ._inidict
@@ -1222,6 +1226,49 @@ def parse(self, args: List[str], addopts: bool = True) -> None:
1222
1226
except PrintHelp :
1223
1227
pass
1224
1228
1229
+ def issue_config_time_warning (self , warning : Warning , stacklevel : int ) -> None :
1230
+ """Issue and handle a warning during the "configure" stage.
1231
+
1232
+ During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
1233
+ function because it is not possible to have hookwrappers around ``pytest_configure``.
1234
+
1235
+ This function is mainly intended for plugins that need to issue warnings during
1236
+ ``pytest_configure`` (or similar stages).
1237
+
1238
+ :param warning: The warning instance.
1239
+ :param stacklevel: stacklevel forwarded to warnings.warn.
1240
+ """
1241
+ if self .pluginmanager .is_blocked ("warnings" ):
1242
+ return
1243
+
1244
+ cmdline_filters = self .known_args_namespace .pythonwarnings or []
1245
+ config_filters = self .getini ("filterwarnings" )
1246
+
1247
+ with warnings .catch_warnings (record = True ) as records :
1248
+ warnings .simplefilter ("always" , type (warning ))
1249
+ apply_warning_filters (config_filters , cmdline_filters )
1250
+ warnings .warn (warning , stacklevel = stacklevel )
1251
+
1252
+ if records :
1253
+ frame = sys ._getframe (stacklevel - 1 )
1254
+ location = frame .f_code .co_filename , frame .f_lineno , frame .f_code .co_name
1255
+ self .hook .pytest_warning_captured .call_historic (
1256
+ kwargs = dict (
1257
+ warning_message = records [0 ],
1258
+ when = "config" ,
1259
+ item = None ,
1260
+ location = location ,
1261
+ )
1262
+ )
1263
+ self .hook .pytest_warning_recorded .call_historic (
1264
+ kwargs = dict (
1265
+ warning_message = records [0 ],
1266
+ when = "config" ,
1267
+ nodeid = "" ,
1268
+ location = location ,
1269
+ )
1270
+ )
1271
+
1225
1272
def addinivalue_line (self , name : str , line : str ) -> None :
1226
1273
"""Add a line to an ini-file option. The option must have been
1227
1274
declared but might not yet be set in which case the line becomes
@@ -1365,8 +1412,6 @@ def getvalueorskip(self, name: str, path=None):
1365
1412
1366
1413
def _warn_about_missing_assertion (self , mode : str ) -> None :
1367
1414
if not _assertion_supported ():
1368
- from _pytest .warnings import _issue_warning_captured
1369
-
1370
1415
if mode == "plain" :
1371
1416
warning_text = (
1372
1417
"ASSERTIONS ARE NOT EXECUTED"
@@ -1381,8 +1426,15 @@ def _warn_about_missing_assertion(self, mode: str) -> None:
1381
1426
"by the underlying Python interpreter "
1382
1427
"(are you using python -O?)\n "
1383
1428
)
1384
- _issue_warning_captured (
1385
- PytestConfigWarning (warning_text ), self .hook , stacklevel = 3 ,
1429
+ self .issue_config_time_warning (
1430
+ PytestConfigWarning (warning_text ), stacklevel = 3 ,
1431
+ )
1432
+
1433
+ def _warn_about_skipped_plugins (self ) -> None :
1434
+ for module_name , msg in self .pluginmanager .skipped_plugins :
1435
+ self .issue_config_time_warning (
1436
+ PytestConfigWarning ("skipped plugin {!r}: {}" .format (module_name , msg )),
1437
+ stacklevel = 2 ,
1386
1438
)
1387
1439
1388
1440
@@ -1435,3 +1487,51 @@ def _strtobool(val: str) -> bool:
1435
1487
return False
1436
1488
else :
1437
1489
raise ValueError ("invalid truth value {!r}" .format (val ))
1490
+
1491
+
1492
+ @lru_cache (maxsize = 50 )
1493
+ def parse_warning_filter (
1494
+ arg : str , * , escape : bool
1495
+ ) -> "Tuple[str, str, Type[Warning], str, int]" :
1496
+ """Parse a warnings filter string.
1497
+
1498
+ This is copied from warnings._setoption, but does not apply the filter,
1499
+ only parses it, and makes the escaping optional.
1500
+ """
1501
+ parts = arg .split (":" )
1502
+ if len (parts ) > 5 :
1503
+ raise warnings ._OptionError ("too many fields (max 5): {!r}" .format (arg ))
1504
+ while len (parts ) < 5 :
1505
+ parts .append ("" )
1506
+ action_ , message , category_ , module , lineno_ = [s .strip () for s in parts ]
1507
+ action = warnings ._getaction (action_ ) # type: str # type: ignore[attr-defined]
1508
+ category = warnings ._getcategory (
1509
+ category_
1510
+ ) # type: Type[Warning] # type: ignore[attr-defined]
1511
+ if message and escape :
1512
+ message = re .escape (message )
1513
+ if module and escape :
1514
+ module = re .escape (module ) + r"\Z"
1515
+ if lineno_ :
1516
+ try :
1517
+ lineno = int (lineno_ )
1518
+ if lineno < 0 :
1519
+ raise ValueError
1520
+ except (ValueError , OverflowError ) as e :
1521
+ raise warnings ._OptionError ("invalid lineno {!r}" .format (lineno_ )) from e
1522
+ else :
1523
+ lineno = 0
1524
+ return action , message , category , module , lineno
1525
+
1526
+
1527
+ def apply_warning_filters (
1528
+ config_filters : Iterable [str ], cmdline_filters : Iterable [str ]
1529
+ ) -> None :
1530
+ """Applies pytest-configured filters to the warnings module"""
1531
+ # Filters should have this precedence: cmdline options, config.
1532
+ # Filters should be applied in the inverse order of precedence.
1533
+ for arg in config_filters :
1534
+ warnings .filterwarnings (* parse_warning_filter (arg , escape = False ))
1535
+
1536
+ for arg in cmdline_filters :
1537
+ warnings .filterwarnings (* parse_warning_filter (arg , escape = True ))
0 commit comments