66import enum
77import inspect
88import os
9+ import re
910import shlex
1011import sys
1112import types
1516from typing import Any
1617from typing import Callable
1718from typing import Dict
19+ from typing import Generator
1820from typing import IO
1921from typing import Iterable
2022from typing import Iterator
@@ -342,6 +344,13 @@ def __init__(self) -> None:
342344 self ._noconftest = False
343345 self ._duplicatepaths = set () # type: Set[py.path.local]
344346
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+
345354 self .add_hookspecs (_pytest .hookspec )
346355 self .register (self )
347356 if os .environ .get ("PYTEST_DEBUG" ):
@@ -694,13 +703,7 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No
694703 ).with_traceback (e .__traceback__ ) from e
695704
696705 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 "" ))
704707 else :
705708 mod = sys .modules [importspec ]
706709 self .register (mod , modname )
@@ -1092,6 +1095,9 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None:
10921095 self ._validate_args (self .getini ("addopts" ), "via addopts config" ) + args
10931096 )
10941097
1098+ self .known_args_namespace = self ._parser .parse_known_args (
1099+ args , namespace = copy .copy (self .option )
1100+ )
10951101 self ._checkversion ()
10961102 self ._consider_importhook (args )
10971103 self .pluginmanager .consider_preparse (args , exclude_only = False )
@@ -1100,10 +1106,10 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None:
11001106 # plugins are going to be loaded.
11011107 self .pluginmanager .load_setuptools_entrypoints ("pytest11" )
11021108 self .pluginmanager .consider_env ()
1103- self .known_args_namespace = ns = self ._parser .parse_known_args (
1104- args , namespace = copy .copy (self .option )
1105- )
1109+
11061110 self ._validate_plugins ()
1111+ self ._warn_about_skipped_plugins ()
1112+
11071113 if self .known_args_namespace .confcutdir is None and self .inifile :
11081114 confcutdir = py .path .local (self .inifile ).dirname
11091115 self .known_args_namespace .confcutdir = confcutdir
@@ -1112,21 +1118,24 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None:
11121118 early_config = self , args = args , parser = self ._parser
11131119 )
11141120 except ConftestImportFailure as e :
1115- if ns . help or ns .version :
1121+ if self . known_args_namespace . help or self . known_args_namespace .version :
11161122 # we don't want to prevent --help/--version to work
11171123 # 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 (
11211125 PytestConfigWarning (
11221126 "could not load initial conftests: {}" .format (e .path )
11231127 ),
1124- self .hook ,
11251128 stacklevel = 2 ,
11261129 )
11271130 else :
11281131 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 ()
11301139
11311140 def _checkversion (self ) -> None :
11321141 import pytest
@@ -1147,9 +1156,9 @@ def _checkversion(self) -> None:
11471156 % (self .inifile , minver , pytest .__version__ ,)
11481157 )
11491158
1150- def _validate_keys (self ) -> None :
1159+ def _validate_config_options (self ) -> None :
11511160 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 ))
11531162
11541163 def _validate_plugins (self ) -> None :
11551164 required_plugins = sorted (self .getini ("required_plugins" ))
@@ -1165,7 +1174,6 @@ def _validate_plugins(self) -> None:
11651174
11661175 missing_plugins = []
11671176 for required_plugin in required_plugins :
1168- spec = None
11691177 try :
11701178 spec = Requirement (required_plugin )
11711179 except InvalidRequirement :
@@ -1187,11 +1195,7 @@ def _warn_or_fail_if_strict(self, message: str) -> None:
11871195 if self .known_args_namespace .strict_config :
11881196 fail (message , pytrace = False )
11891197
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 )
11951199
11961200 def _get_unknown_ini_keys (self ) -> List [str ]:
11971201 parser_inicfg = self ._parser ._inidict
@@ -1222,6 +1226,49 @@ def parse(self, args: List[str], addopts: bool = True) -> None:
12221226 except PrintHelp :
12231227 pass
12241228
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+
12251272 def addinivalue_line (self , name : str , line : str ) -> None :
12261273 """Add a line to an ini-file option. The option must have been
12271274 declared but might not yet be set in which case the line becomes
@@ -1365,8 +1412,6 @@ def getvalueorskip(self, name: str, path=None):
13651412
13661413 def _warn_about_missing_assertion (self , mode : str ) -> None :
13671414 if not _assertion_supported ():
1368- from _pytest .warnings import _issue_warning_captured
1369-
13701415 if mode == "plain" :
13711416 warning_text = (
13721417 "ASSERTIONS ARE NOT EXECUTED"
@@ -1381,8 +1426,15 @@ def _warn_about_missing_assertion(self, mode: str) -> None:
13811426 "by the underlying Python interpreter "
13821427 "(are you using python -O?)\n "
13831428 )
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 ,
13861438 )
13871439
13881440
@@ -1435,3 +1487,51 @@ def _strtobool(val: str) -> bool:
14351487 return False
14361488 else :
14371489 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