aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/pytest/py3/_pytest/logging.py
diff options
context:
space:
mode:
authorrobot-piglet <robot-piglet@yandex-team.com>2025-05-05 12:31:52 +0300
committerrobot-piglet <robot-piglet@yandex-team.com>2025-05-05 12:41:33 +0300
commit6ff49ec58061f642c3a2f83c61eba12820787dfc (patch)
treec733ec9bdb15ed280080d31dea8725bfec717acd /contrib/python/pytest/py3/_pytest/logging.py
parenteefca8305c6a545cc6b16dca3eb0d91dcef2adcd (diff)
downloadydb-6ff49ec58061f642c3a2f83c61eba12820787dfc.tar.gz
Intermediate changes
commit_hash:8b3bb826b17db8329ed1221f545c0645f12c552d
Diffstat (limited to 'contrib/python/pytest/py3/_pytest/logging.py')
-rw-r--r--contrib/python/pytest/py3/_pytest/logging.py118
1 files changed, 73 insertions, 45 deletions
diff --git a/contrib/python/pytest/py3/_pytest/logging.py b/contrib/python/pytest/py3/_pytest/logging.py
index 9f2f1c79359..ad7c2dfff42 100644
--- a/contrib/python/pytest/py3/_pytest/logging.py
+++ b/contrib/python/pytest/py3/_pytest/logging.py
@@ -1,23 +1,29 @@
"""Access and control log capturing."""
-import io
-import logging
-import os
-import re
+
from contextlib import contextmanager
from contextlib import nullcontext
from datetime import datetime
from datetime import timedelta
from datetime import timezone
+import io
from io import StringIO
+import logging
from logging import LogRecord
+import os
from pathlib import Path
+import re
+from types import TracebackType
from typing import AbstractSet
from typing import Dict
+from typing import final
from typing import Generator
+from typing import Generic
from typing import List
+from typing import Literal
from typing import Mapping
from typing import Optional
from typing import Tuple
+from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
@@ -25,7 +31,6 @@ from typing import Union
from _pytest import nodes
from _pytest._io import TerminalWriter
from _pytest.capture import CaptureManager
-from _pytest.compat import final
from _pytest.config import _strtobool
from _pytest.config import Config
from _pytest.config import create_terminal_writer
@@ -39,10 +44,9 @@ from _pytest.main import Session
from _pytest.stash import StashKey
from _pytest.terminal import TerminalReporter
+
if TYPE_CHECKING:
logging_StreamHandler = logging.StreamHandler[StringIO]
-
- from typing_extensions import Literal
else:
logging_StreamHandler = logging.StreamHandler
@@ -63,13 +67,14 @@ class DatetimeFormatter(logging.Formatter):
:func:`time.strftime` in case of microseconds in format string.
"""
- def formatTime(self, record: LogRecord, datefmt=None) -> str:
+ def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str:
if datefmt and "%f" in datefmt:
ct = self.converter(record.created)
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
# Construct `datetime.datetime` object from `struct_time`
# and msecs information from `record`
- dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
+ # Using int() instead of round() to avoid it exceeding 1_000_000 and causing a ValueError (#11861).
+ dt = datetime(*ct[0:6], microsecond=int(record.msecs * 1000), tzinfo=tz)
return dt.strftime(datefmt)
# Use `logging.Formatter` for non-microsecond formats
return super().formatTime(record, datefmt)
@@ -112,7 +117,6 @@ class ColoredLevelFormatter(DatetimeFormatter):
.. warning::
This is an experimental API.
"""
-
assert self._fmt is not None
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
if not levelname_fmt_match:
@@ -179,7 +183,6 @@ class PercentStyleMultiline(logging.PercentStyle):
0 (auto-indent turned off) or
>0 (explicitly set indentation position).
"""
-
if auto_indent_option is None:
return 0
elif isinstance(auto_indent_option, bool):
@@ -304,13 +307,13 @@ def pytest_addoption(parser: Parser) -> None:
add_option_ini(
"--log-file-format",
dest="log_file_format",
- default=DEFAULT_LOG_FORMAT,
+ default=None,
help="Log format used by the logging module",
)
add_option_ini(
"--log-file-date-format",
dest="log_file_date_format",
- default=DEFAULT_LOG_DATE_FORMAT,
+ default=None,
help="Log date format used by the logging module",
)
add_option_ini(
@@ -332,7 +335,7 @@ _HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
# Not using @contextmanager for performance reasons.
-class catching_logs:
+class catching_logs(Generic[_HandlerType]):
"""Context manager that prepares the whole logging machinery properly."""
__slots__ = ("handler", "level", "orig_level")
@@ -341,7 +344,7 @@ class catching_logs:
self.handler = handler
self.level = level
- def __enter__(self):
+ def __enter__(self) -> _HandlerType:
root_logger = logging.getLogger()
if self.level is not None:
self.handler.setLevel(self.level)
@@ -351,7 +354,12 @@ class catching_logs:
root_logger.setLevel(min(self.orig_level, self.level))
return self.handler
- def __exit__(self, type, value, traceback):
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
root_logger = logging.getLogger()
if self.level is not None:
root_logger.setLevel(self.orig_level)
@@ -422,7 +430,7 @@ class LogCaptureFixture:
return self._item.stash[caplog_handler_key]
def get_records(
- self, when: "Literal['setup', 'call', 'teardown']"
+ self, when: Literal["setup", "call", "teardown"]
) -> List[logging.LogRecord]:
"""Get the logging records for one of the possible test phases.
@@ -523,7 +531,7 @@ class LogCaptureFixture:
The levels of the loggers changed by this function will be
restored to their initial values at the end of the test.
- Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
+ Will enable the requested logging level if it was disabled via :func:`logging.disable`.
:param level: The level.
:param logger: The logger to update. If not given, the root logger.
@@ -547,7 +555,7 @@ class LogCaptureFixture:
the end of the 'with' statement the level is restored to its original
value.
- Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
+ Will enable the requested logging level if it was disabled via :func:`logging.disable`.
:param level: The level.
:param logger: The logger to update. If not given, the root logger.
@@ -565,6 +573,22 @@ class LogCaptureFixture:
self.handler.setLevel(handler_orig_level)
logging.disable(original_disable_level)
+ @contextmanager
+ def filtering(self, filter_: logging.Filter) -> Generator[None, None, None]:
+ """Context manager that temporarily adds the given filter to the caplog's
+ :meth:`handler` for the 'with' statement block, and removes that filter at the
+ end of the block.
+
+ :param filter_: A custom :class:`logging.Filter` object.
+
+ .. versionadded:: 7.5
+ """
+ self.handler.addFilter(filter_)
+ try:
+ yield
+ finally:
+ self.handler.removeFilter(filter_)
+
@fixture
def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
@@ -600,9 +624,9 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i
except ValueError as e:
# Python logging does not recognise this as a logging level
raise UsageError(
- "'{}' is not recognized as a logging level name for "
- "'{}'. Please consider passing the "
- "logging level num instead.".format(log_level, setting_name)
+ f"'{log_level}' is not recognized as a logging level name for "
+ f"'{setting_name}'. Please consider passing the "
+ "logging level num instead."
) from e
@@ -636,7 +660,9 @@ class LoggingPlugin:
self.report_handler.setFormatter(self.formatter)
# File logging.
- self.log_file_level = get_log_level_for_setting(config, "log_file_level")
+ self.log_file_level = get_log_level_for_setting(
+ config, "log_file_level", "log_level"
+ )
log_file = get_option_ini(config, "log_file") or os.devnull
if log_file != os.devnull:
directory = os.path.dirname(os.path.abspath(log_file))
@@ -725,7 +751,7 @@ class LoggingPlugin:
if old_stream:
old_stream.close()
- def _log_cli_enabled(self):
+ def _log_cli_enabled(self) -> bool:
"""Return whether live logging is enabled."""
enabled = self._config.getoption(
"--log-cli-level"
@@ -740,27 +766,26 @@ class LoggingPlugin:
return True
- @hookimpl(hookwrapper=True, tryfirst=True)
+ @hookimpl(wrapper=True, tryfirst=True)
def pytest_sessionstart(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("sessionstart")
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level):
- yield
+ return (yield)
- @hookimpl(hookwrapper=True, tryfirst=True)
+ @hookimpl(wrapper=True, tryfirst=True)
def pytest_collection(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("collection")
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level):
- yield
+ return (yield)
- @hookimpl(hookwrapper=True)
- def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
+ @hookimpl(wrapper=True)
+ def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]:
if session.config.option.collectonly:
- yield
- return
+ return (yield)
if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
# The verbose flag is needed to avoid messy test progress output.
@@ -768,7 +793,7 @@ class LoggingPlugin:
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level):
- yield # Run all the tests.
+ return (yield) # Run all the tests.
@hookimpl
def pytest_runtest_logstart(self) -> None:
@@ -793,12 +818,13 @@ class LoggingPlugin:
item.stash[caplog_records_key][when] = caplog_handler.records
item.stash[caplog_handler_key] = caplog_handler
- yield
-
- log = report_handler.stream.getvalue().strip()
- item.add_report_section(when, "log", log)
+ try:
+ yield
+ finally:
+ log = report_handler.stream.getvalue().strip()
+ item.add_report_section(when, "log", log)
- @hookimpl(hookwrapper=True)
+ @hookimpl(wrapper=True)
def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("setup")
@@ -806,31 +832,33 @@ class LoggingPlugin:
item.stash[caplog_records_key] = empty
yield from self._runtest_for(item, "setup")
- @hookimpl(hookwrapper=True)
+ @hookimpl(wrapper=True)
def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("call")
yield from self._runtest_for(item, "call")
- @hookimpl(hookwrapper=True)
+ @hookimpl(wrapper=True)
def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("teardown")
- yield from self._runtest_for(item, "teardown")
- del item.stash[caplog_records_key]
- del item.stash[caplog_handler_key]
+ try:
+ yield from self._runtest_for(item, "teardown")
+ finally:
+ del item.stash[caplog_records_key]
+ del item.stash[caplog_handler_key]
@hookimpl
def pytest_runtest_logfinish(self) -> None:
self.log_cli_handler.set_when("finish")
- @hookimpl(hookwrapper=True, tryfirst=True)
+ @hookimpl(wrapper=True, tryfirst=True)
def pytest_sessionfinish(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("sessionfinish")
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level):
- yield
+ return (yield)
@hookimpl
def pytest_unconfigure(self) -> None: