diff options
author | arcadia-devtools <arcadia-devtools@yandex-team.ru> | 2022-02-14 00:49:36 +0300 |
---|---|---|
committer | arcadia-devtools <arcadia-devtools@yandex-team.ru> | 2022-02-14 00:49:36 +0300 |
commit | 82cfd1b7cab2d843cdf5467d9737f72597a493bd (patch) | |
tree | 1dfdcfe81a1a6b193ceacc2a828c521b657a339b /contrib/python/pytest/py3/_pytest/config/__init__.py | |
parent | 3df7211d3e3691f8e33b0a1fb1764fe810d59302 (diff) | |
download | ydb-82cfd1b7cab2d843cdf5467d9737f72597a493bd.tar.gz |
intermediate changes
ref:68b1302de4b5da30b6bdf02193f7a2604d8b5cf8
Diffstat (limited to 'contrib/python/pytest/py3/_pytest/config/__init__.py')
-rw-r--r-- | contrib/python/pytest/py3/_pytest/config/__init__.py | 351 |
1 files changed, 221 insertions, 130 deletions
diff --git a/contrib/python/pytest/py3/_pytest/config/__init__.py b/contrib/python/pytest/py3/_pytest/config/__init__.py index bd9e2883f9..ebf6e1b950 100644 --- a/contrib/python/pytest/py3/_pytest/config/__init__.py +++ b/contrib/python/pytest/py3/_pytest/config/__init__.py @@ -13,9 +13,11 @@ import types import warnings from functools import lru_cache from pathlib import Path +from textwrap import dedent from types import TracebackType from typing import Any from typing import Callable +from typing import cast from typing import Dict from typing import Generator from typing import IO @@ -32,7 +34,6 @@ from typing import TYPE_CHECKING from typing import Union import attr -import py from pluggy import HookimplMarker from pluggy import HookspecMarker from pluggy import PluginManager @@ -50,10 +51,12 @@ from _pytest.compat import final from _pytest.compat import importlib_metadata from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath from _pytest.pathlib import import_path from _pytest.pathlib import ImportMode -from _pytest.store import Store +from _pytest.pathlib import resolve_package_path +from _pytest.stash import Stash from _pytest.warning_types import PytestConfigWarning if TYPE_CHECKING: @@ -103,7 +106,7 @@ class ExitCode(enum.IntEnum): class ConftestImportFailure(Exception): def __init__( self, - path: py.path.local, + path: Path, excinfo: Tuple[Type[Exception], Exception, TracebackType], ) -> None: super().__init__(path, excinfo) @@ -128,7 +131,7 @@ def filter_traceback_for_conftest_import_failure( def main( - args: Optional[Union[List[str], py.path.local]] = None, + args: Optional[Union[List[str], "os.PathLike[str]"]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> Union[int, ExitCode]: """Perform an in-process test run. @@ -142,7 +145,7 @@ def main( try: config = _prepareconfig(args, plugins) except ConftestImportFailure as e: - exc_info = ExceptionInfo(e.excinfo) + exc_info = ExceptionInfo.from_exc_info(e.excinfo) tw = TerminalWriter(sys.stderr) tw.line(f"ImportError while loading conftest '{e.path}'.", red=True) exc_info.traceback = exc_info.traceback.filter( @@ -235,6 +238,7 @@ default_plugins = essential_plugins + ( "unittest", "capture", "skipping", + "legacypath", "tmpdir", "monkeypatch", "recwarn", @@ -251,6 +255,7 @@ default_plugins = essential_plugins + ( "warnings", "logging", "reports", + "python_path", *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), "faulthandler", ) @@ -269,7 +274,9 @@ def get_config( config = Config( pluginmanager, invocation_params=Config.InvocationParams( - args=args or (), plugins=plugins, dir=Path.cwd(), + args=args or (), + plugins=plugins, + dir=Path.cwd(), ), ) @@ -285,7 +292,7 @@ def get_config( def get_plugin_manager() -> "PytestPluginManager": """Obtain a new instance of the - :py:class:`_pytest.config.PytestPluginManager`, with default plugins + :py:class:`pytest.PytestPluginManager`, with default plugins already loaded. This function can be used by integration with other tools, like hooking @@ -295,13 +302,13 @@ def get_plugin_manager() -> "PytestPluginManager": def _prepareconfig( - args: Optional[Union[py.path.local, List[str]]] = None, + args: Optional[Union[List[str], "os.PathLike[str]"]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> "Config": if args is None: args = sys.argv[1:] - elif isinstance(args, py.path.local): - args = [str(args)] + elif isinstance(args, os.PathLike): + args = [os.fspath(args)] elif not isinstance(args, list): msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})" raise TypeError(msg.format(args, type(args))) @@ -324,6 +331,14 @@ def _prepareconfig( raise +def _get_directory(path: Path) -> Path: + """Get the directory of a path - itself if already a directory.""" + if path.is_file(): + return path.parent + else: + return path + + @final class PytestPluginManager(PluginManager): """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with @@ -342,11 +357,17 @@ class PytestPluginManager(PluginManager): self._conftest_plugins: Set[types.ModuleType] = set() # State related to local conftest plugins. - self._dirpath2confmods: Dict[py.path.local, List[types.ModuleType]] = {} + self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {} self._conftestpath2mod: Dict[Path, types.ModuleType] = {} - self._confcutdir: Optional[py.path.local] = None + self._confcutdir: Optional[Path] = None self._noconftest = False - self._duplicatepaths: Set[py.path.local] = set() + + # _getconftestmodules()'s call to _get_directory() causes a stat + # storm when it's called potentially thousands of times in a test + # session (#9478), often with the same path, so cache it. + self._get_directory = lru_cache(256)(_get_directory) + + self._duplicatepaths: Set[Path] = set() # plugins that were explicitly skipped with pytest.skip # list of (module name, skip reason) @@ -362,7 +383,10 @@ class PytestPluginManager(PluginManager): encoding: str = getattr(err, "encoding", "utf8") try: err = open( - os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding, + os.dup(err.fileno()), + mode=err.mode, + buffering=1, + encoding=encoding, ) except Exception: pass @@ -471,7 +495,9 @@ class PytestPluginManager(PluginManager): # # Internal API for local conftest plugin handling. # - def _set_initial_conftests(self, namespace: argparse.Namespace) -> None: + def _set_initial_conftests( + self, namespace: argparse.Namespace, rootpath: Path + ) -> None: """Load initial conftest files given a preparsed "namespace". As conftest files may add their own command line options which have @@ -479,9 +505,9 @@ class PytestPluginManager(PluginManager): All builtin and 3rd party plugins will have been loaded, however, so common options will not confuse our logic here. """ - current = py.path.local() + current = Path.cwd() self._confcutdir = ( - current.join(namespace.confcutdir, abs=True) + absolutepath(current / namespace.confcutdir) if namespace.confcutdir else None ) @@ -495,53 +521,60 @@ class PytestPluginManager(PluginManager): i = path.find("::") if i != -1: path = path[:i] - anchor = current.join(path, abs=1) + anchor = absolutepath(current / path) if anchor.exists(): # we found some file object - self._try_load_conftest(anchor, namespace.importmode) + self._try_load_conftest(anchor, namespace.importmode, rootpath) foundanchor = True if not foundanchor: - self._try_load_conftest(current, namespace.importmode) + self._try_load_conftest(current, namespace.importmode, rootpath) def _try_load_conftest( - self, anchor: py.path.local, importmode: Union[str, ImportMode] + self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path ) -> None: - self._getconftestmodules(anchor, importmode) + self._getconftestmodules(anchor, importmode, rootpath) # let's also consider test* subdirs - if anchor.check(dir=1): - for x in anchor.listdir("test*"): - if x.check(dir=1): - self._getconftestmodules(x, importmode) + if anchor.is_dir(): + for x in anchor.glob("test*"): + if x.is_dir(): + self._getconftestmodules(x, importmode, rootpath) - @lru_cache(maxsize=128) def _getconftestmodules( - self, path: py.path.local, importmode: Union[str, ImportMode], + self, path: Path, importmode: Union[str, ImportMode], rootpath: Path ) -> List[types.ModuleType]: if self._noconftest: return [] - if path.isfile(): - directory = path.dirpath() - else: - directory = path + directory = self._get_directory(path) + + # Optimization: avoid repeated searches in the same directory. + # Assumes always called with same importmode and rootpath. + existing_clist = self._dirpath2confmods.get(directory) + if existing_clist is not None: + return existing_clist # XXX these days we may rather want to use config.rootpath # and allow users to opt into looking into the rootdir parent # directories instead of requiring to specify confcutdir. clist = [] - for parent in directory.parts(): - if self._confcutdir and self._confcutdir.relto(parent): + confcutdir_parents = self._confcutdir.parents if self._confcutdir else [] + for parent in reversed((directory, *directory.parents)): + if parent in confcutdir_parents: continue - conftestpath = parent.join("conftest.py") - if conftestpath.isfile(): - mod = self._importconftest(conftestpath, importmode) + conftestpath = parent / "conftest.py" + if conftestpath.is_file(): + mod = self._importconftest(conftestpath, importmode, rootpath) clist.append(mod) self._dirpath2confmods[directory] = clist return clist def _rget_with_confmod( - self, name: str, path: py.path.local, importmode: Union[str, ImportMode], + self, + name: str, + path: Path, + importmode: Union[str, ImportMode], + rootpath: Path, ) -> Tuple[types.ModuleType, Any]: - modules = self._getconftestmodules(path, importmode) + modules = self._getconftestmodules(path, importmode, rootpath=rootpath) for mod in reversed(modules): try: return mod, getattr(mod, name) @@ -550,24 +583,24 @@ class PytestPluginManager(PluginManager): raise KeyError(name) def _importconftest( - self, conftestpath: py.path.local, importmode: Union[str, ImportMode], + self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path ) -> types.ModuleType: # Use a resolved Path object as key to avoid loading the same conftest # twice with build systems that create build directories containing # symlinks to actual files. # Using Path().resolve() is better than py.path.realpath because # it resolves to the correct path/drive in case-insensitive file systems (#5792) - key = Path(str(conftestpath)).resolve() + key = conftestpath.resolve() with contextlib.suppress(KeyError): return self._conftestpath2mod[key] - pkgpath = conftestpath.pypkgpath() + pkgpath = resolve_package_path(conftestpath) if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) + _ensure_removed_sysmodule(conftestpath.stem) try: - mod = import_path(conftestpath, mode=importmode) + mod = import_path(conftestpath, mode=importmode, root=rootpath) except Exception as e: assert e.__traceback__ is not None exc_info = (type(e), e, e.__traceback__) @@ -577,10 +610,10 @@ class PytestPluginManager(PluginManager): self._conftest_plugins.add(mod) self._conftestpath2mod[key] = mod - dirpath = conftestpath.dirpath() + dirpath = conftestpath.parent if dirpath in self._dirpath2confmods: for path, mods in self._dirpath2confmods.items(): - if path and path.relto(dirpath) or path == dirpath: + if path and dirpath in path.parents or path == dirpath: assert mod not in mods mods.append(mod) self.trace(f"loading conftestmodule {mod!r}") @@ -588,7 +621,9 @@ class PytestPluginManager(PluginManager): return mod def _check_non_top_pytest_plugins( - self, mod: types.ModuleType, conftestpath: py.path.local, + self, + mod: types.ModuleType, + conftestpath: Path, ) -> None: if ( hasattr(mod, "pytest_plugins") @@ -614,6 +649,7 @@ class PytestPluginManager(PluginManager): def consider_preparse( self, args: Sequence[str], *, exclude_only: bool = False ) -> None: + """:meta private:""" i = 0 n = len(args) while i < n: @@ -635,6 +671,7 @@ class PytestPluginManager(PluginManager): self.consider_pluginarg(parg) def consider_pluginarg(self, arg: str) -> None: + """:meta private:""" if arg.startswith("no:"): name = arg[3:] if name in essential_plugins: @@ -660,12 +697,15 @@ class PytestPluginManager(PluginManager): self.import_plugin(arg, consider_entry_points=True) def consider_conftest(self, conftestmodule: types.ModuleType) -> None: + """:meta private:""" self.register(conftestmodule, name=conftestmodule.__file__) def consider_env(self) -> None: + """:meta private:""" self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) def consider_module(self, mod: types.ModuleType) -> None: + """:meta private:""" self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) def _import_plugin_specs( @@ -703,7 +743,7 @@ class PytestPluginManager(PluginManager): __import__(importspec) except ImportError as e: raise ImportError( - 'Error importing plugin "{}": {}'.format(modname, str(e.args[0])) + f'Error importing plugin "{modname}": {e.args[0]}' ).with_traceback(e.__traceback__) from e except Skipped as e: @@ -823,6 +863,7 @@ class Config: """Access to configuration values, pluginmanager and plugin hooks. :param PytestPluginManager pluginmanager: + A pytest PluginManager. :param InvocationParams invocation_params: Object containing parameters regarding the :func:`pytest.main` @@ -830,7 +871,7 @@ class Config: """ @final - @attr.s(frozen=True) + @attr.s(frozen=True, auto_attribs=True) class InvocationParams: """Holds parameters passed during :func:`pytest.main`. @@ -846,21 +887,12 @@ class Config: Plugins accessing ``InvocationParams`` must be aware of that. """ - args = attr.ib(type=Tuple[str, ...], converter=_args_converter) - """The command-line arguments as passed to :func:`pytest.main`. - - :type: Tuple[str, ...] - """ - plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]]) - """Extra plugins, might be `None`. - - :type: Optional[Sequence[Union[str, plugin]]] - """ - dir = attr.ib(type=Path) - """The directory from which :func:`pytest.main` was invoked. - - :type: pathlib.Path - """ + args: Tuple[str, ...] = attr.ib(converter=_args_converter) + """The command-line arguments as passed to :func:`pytest.main`.""" + plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] + """Extra plugins, might be `None`.""" + dir: Path + """The directory from which :func:`pytest.main` was invoked.""" def __init__( self, @@ -891,6 +923,7 @@ class Config: self._parser = Parser( usage=f"%(prog)s [options] [{_a}] [{_a}] [...]", processopt=self._processopt, + _ispytest=True, ) self.pluginmanager = pluginmanager """The plugin manager handles plugin registration and hook invocation. @@ -898,15 +931,23 @@ class Config: :type: PytestPluginManager """ + self.stash = Stash() + """A place where plugins can store information on the config for their + own use. + + :type: Stash + """ + # Deprecated alias. Was never public. Can be removed in a few releases. + self._store = self.stash + + from .compat import PathAwareHookProxy + self.trace = self.pluginmanager.trace.root.get("config") - self.hook = self.pluginmanager.hook + self.hook = PathAwareHookProxy(self.pluginmanager.hook) self._inicache: Dict[str, Any] = {} self._override_ini: Sequence[str] = () self._opt2dest: Dict[str, str] = {} self._cleanup: List[Callable[[], None]] = [] - # A place where plugins can store information on the config for their - # own use. Currently only intended for internal plugins. - self._store = Store() self.pluginmanager.register(self, "pytestconfig") self._configured = False self.hook.pytest_addoption.call_historic( @@ -919,17 +960,6 @@ class Config: self.cache: Optional[Cache] = None @property - def invocation_dir(self) -> py.path.local: - """The directory from which pytest was invoked. - - Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`, - which is a :class:`pathlib.Path`. - - :type: py.path.local - """ - return py.path.local(str(self.invocation_params.dir)) - - @property def rootpath(self) -> Path: """The path to the :ref:`rootdir <rootdir>`. @@ -940,16 +970,6 @@ class Config: return self._rootpath @property - def rootdir(self) -> py.path.local: - """The path to the :ref:`rootdir <rootdir>`. - - Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. - - :type: py.path.local - """ - return py.path.local(str(self.rootpath)) - - @property def inipath(self) -> Optional[Path]: """The path to the :ref:`configfile <configfiles>`. @@ -959,19 +979,9 @@ class Config: """ return self._inipath - @property - def inifile(self) -> Optional[py.path.local]: - """The path to the :ref:`configfile <configfiles>`. - - Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. - - :type: Optional[py.path.local] - """ - return py.path.local(str(self.inipath)) if self.inipath else None - def add_cleanup(self, func: Callable[[], None]) -> None: """Add a function to be called when the config object gets out of - use (usually coninciding with pytest_unconfigure).""" + use (usually coinciding with pytest_unconfigure).""" self._cleanup.append(func) def _do_configure(self) -> None: @@ -1067,7 +1077,9 @@ class Config: @hookimpl(trylast=True) def pytest_load_initial_conftests(self, early_config: "Config") -> None: - self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) + self.pluginmanager._set_initial_conftests( + early_config.known_args_namespace, rootpath=early_config.rootpath + ) def _initini(self, args: Sequence[str]) -> None: ns, unknown_args = self._parser.parse_known_and_unknown_args( @@ -1204,8 +1216,8 @@ class Config: @hookimpl(hookwrapper=True) def pytest_collection(self) -> Generator[None, None, None]: - """Validate invalid ini keys after collection is done so we take in account - options added by late-loading conftest files.""" + # Validate invalid ini keys after collection is done so we take in account + # options added by late-loading conftest files. yield self._validate_config_options() @@ -1225,7 +1237,11 @@ class Config: if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( "%s: 'minversion' requires pytest-%s, actual pytest-%s'" - % (self.inipath, minver, pytest.__version__,) + % ( + self.inipath, + minver, + pytest.__version__, + ) ) def _validate_config_options(self) -> None: @@ -1247,14 +1263,16 @@ class Config: missing_plugins = [] for required_plugin in required_plugins: try: - spec = Requirement(required_plugin) + req = Requirement(required_plugin) except InvalidRequirement: missing_plugins.append(required_plugin) continue - if spec.name not in plugin_dist_info: + if req.name not in plugin_dist_info: missing_plugins.append(required_plugin) - elif Version(plugin_dist_info[spec.name]) not in spec.specifier: + elif not req.specifier.contains( + Version(plugin_dist_info[req.name]), prereleases=True + ): missing_plugins.append(required_plugin) if missing_plugins: @@ -1352,8 +1370,8 @@ class Config: """Return configuration value from an :ref:`ini file <configfiles>`. If the specified name hasn't been registered through a prior - :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` - call (usually from a plugin), a ValueError is raised. + :func:`parser.addini <pytest.Parser.addini>` call (usually from a + plugin), a ValueError is raised. """ try: return self._inicache[name] @@ -1361,6 +1379,12 @@ class Config: self._inicache[name] = val = self._getini(name) return val + # Meant for easy monkeypatching by legacypath plugin. + # Can be inlined back (with no cover removed) once legacypath is gone. + def _getini_unknown_type(self, name: str, type: str, value: Union[str, List[str]]): + msg = f"unknown configuration type: {type}" + raise ValueError(msg, value) # pragma: no cover + def _getini(self, name: str): try: description, type, default = self._parser._inidict[name] @@ -1393,12 +1417,12 @@ class Config: # a_line_list = ["tests", "acceptance"] # in this case, we already have a list ready to use. # - if type == "pathlist": + if type == "paths": # TODO: This assert is probably not valid in all cases. assert self.inipath is not None dp = self.inipath.parent input_values = shlex.split(value) if isinstance(value, str) else value - return [py.path.local(str(dp / x)) for x in input_values] + return [dp / x for x in input_values] elif type == "args": return shlex.split(value) if isinstance(value, str) else value elif type == "linelist": @@ -1408,25 +1432,30 @@ class Config: return value elif type == "bool": return _strtobool(str(value).strip()) - else: - assert type in [None, "string"] + elif type == "string": + return value + elif type is None: return value + else: + return self._getini_unknown_type(name, type, value) def _getconftest_pathlist( - self, name: str, path: py.path.local - ) -> Optional[List[py.path.local]]: + self, name: str, path: Path, rootpath: Path + ) -> Optional[List[Path]]: try: mod, relroots = self.pluginmanager._rget_with_confmod( - name, path, self.getoption("importmode") + name, path, self.getoption("importmode"), rootpath ) except KeyError: return None - modpath = py.path.local(mod.__file__).dirpath() - values: List[py.path.local] = [] + modpath = Path(mod.__file__).parent + values: List[Path] = [] for relroot in relroots: - if not isinstance(relroot, py.path.local): + if isinstance(relroot, os.PathLike): + relroot = Path(relroot) + else: relroot = relroot.replace("/", os.sep) - relroot = modpath.join(relroot, abs=True) + relroot = absolutepath(modpath / relroot) values.append(relroot) return values @@ -1498,7 +1527,8 @@ class Config: "(are you using python -O?)\n" ) self.issue_config_time_warning( - PytestConfigWarning(warning_text), stacklevel=3, + PytestConfigWarning(warning_text), + stacklevel=3, ) def _warn_about_skipped_plugins(self) -> None: @@ -1566,17 +1596,54 @@ def parse_warning_filter( ) -> Tuple[str, str, Type[Warning], str, int]: """Parse a warnings filter string. - This is copied from warnings._setoption, but does not apply the filter, - only parses it, and makes the escaping optional. + This is copied from warnings._setoption with the following changes: + + * Does not apply the filter. + * Escaping is optional. + * Raises UsageError so we get nice error messages on failure. """ + __tracebackhide__ = True + error_template = dedent( + f"""\ + while parsing the following warning configuration: + + {arg} + + This error occurred: + + {{error}} + """ + ) + parts = arg.split(":") if len(parts) > 5: - raise warnings._OptionError(f"too many fields (max 5): {arg!r}") + doc_url = ( + "https://docs.python.org/3/library/warnings.html#describing-warning-filters" + ) + error = dedent( + f"""\ + Too many fields ({len(parts)}), expected at most 5 separated by colons: + + action:message:category:module:line + + For more information please consult: {doc_url} + """ + ) + raise UsageError(error_template.format(error=error)) + while len(parts) < 5: parts.append("") - action_, message, category_, module, lineno_ = [s.strip() for s in parts] - action: str = warnings._getaction(action_) # type: ignore[attr-defined] - category: Type[Warning] = warnings._getcategory(category_) # type: ignore[attr-defined] + action_, message, category_, module, lineno_ = (s.strip() for s in parts) + try: + action: str = warnings._getaction(action_) # type: ignore[attr-defined] + except warnings._OptionError as e: + raise UsageError(error_template.format(error=str(e))) + try: + category: Type[Warning] = _resolve_warning_category(category_) + except Exception: + exc_info = ExceptionInfo.from_current() + exception_text = exc_info.getrepr(style="native") + raise UsageError(error_template.format(error=exception_text)) if message and escape: message = re.escape(message) if module and escape: @@ -1585,14 +1652,38 @@ def parse_warning_filter( try: lineno = int(lineno_) if lineno < 0: - raise ValueError - except (ValueError, OverflowError) as e: - raise warnings._OptionError(f"invalid lineno {lineno_!r}") from e + raise ValueError("number is negative") + except ValueError as e: + raise UsageError( + error_template.format(error=f"invalid lineno {lineno_!r}: {e}") + ) else: lineno = 0 return action, message, category, module, lineno +def _resolve_warning_category(category: str) -> Type[Warning]: + """ + Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors) + propagate so we can get access to their tracebacks (#9218). + """ + __tracebackhide__ = True + if not category: + return Warning + + if "." not in category: + import builtins as m + + klass = category + else: + module, _, klass = category.rpartition(".") + m = __import__(module, None, None, [klass]) + cat = getattr(m, klass) + if not issubclass(cat, Warning): + raise UsageError(f"{cat} is not a Warning subclass") + return cast(Type[Warning], cat) + + def apply_warning_filters( config_filters: Iterable[str], cmdline_filters: Iterable[str] ) -> None: |