Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-testfixtures for
openSUSE:Factory checked in at 2026-07-08 17:32:17
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-testfixtures (Old)
and /work/SRC/openSUSE:Factory/.python-testfixtures.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-testfixtures"
Wed Jul 8 17:32:17 2026 rev:33 rq:1364092 version:12.2.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-testfixtures/python-testfixtures.changes
2026-06-19 17:22:03.183471423 +0200
+++
/work/SRC/openSUSE:Factory/.python-testfixtures.new.1982/python-testfixtures.changes
2026-07-08 17:32:25.600871442 +0200
@@ -1,0 +2,9 @@
+Mon Jul 6 06:42:48 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 12.2.0:
+ * Added :meth:`LogCapture.check_empty`.
+ * Added :meth:`LogCapture.disabled` context manager.
+ * Added level and predicate parameters to
+ :meth:`LogCapture.check`.
+
+-------------------------------------------------------------------
Old:
----
testfixtures-12.1.0.tar.gz
New:
----
testfixtures-12.2.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-testfixtures.spec ++++++
--- /var/tmp/diff_new_pack.bLDPd7/_old 2026-07-08 17:32:27.068922479 +0200
+++ /var/tmp/diff_new_pack.bLDPd7/_new 2026-07-08 17:32:27.072922617 +0200
@@ -34,7 +34,7 @@
%{?sle15_python_module_pythons}
Name: python-testfixtures%{psuffix}
-Version: 12.1.0
+Version: 12.2.0
Release: 0
Summary: A collection of helpers and mock objects for unit tests and
doc tests
License: MIT
++++++ testfixtures-12.1.0.tar.gz -> testfixtures-12.2.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/testfixtures-12.1.0/CHANGELOG.rst
new/testfixtures-12.2.0/CHANGELOG.rst
--- old/testfixtures-12.1.0/CHANGELOG.rst 2026-06-15 17:22:45.000000000
+0200
+++ new/testfixtures-12.2.0/CHANGELOG.rst 2026-06-20 11:53:06.000000000
+0200
@@ -3,6 +3,15 @@
Changes
=======
+12.2.0 (20 Jun 2026)
+--------------------
+
+- Added :meth:`LogCapture.check_empty`.
+
+- Added :meth:`LogCapture.disabled` context manager.
+
+- Added ``level`` and ``predicate`` parameters to :meth:`LogCapture.check`.
+
12.1.0 (15 Jun 2026)
--------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/testfixtures-12.1.0/PKG-INFO
new/testfixtures-12.2.0/PKG-INFO
--- old/testfixtures-12.1.0/PKG-INFO 2026-06-15 17:22:45.000000000 +0200
+++ new/testfixtures-12.2.0/PKG-INFO 2026-06-20 11:53:06.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: testfixtures
-Version: 12.1.0
+Version: 12.2.0
Summary: A collection of helpers and mock objects for unit tests and doc tests.
Project-URL: Homepage, https://github.com/Simplistix/testfixtures
Project-URL: Documentation, http://testfixtures.readthedocs.org/en/latest/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/testfixtures-12.1.0/docs/logging.rst
new/testfixtures-12.2.0/docs/logging.rst
--- old/testfixtures-12.1.0/docs/logging.rst 2026-06-15 17:22:45.000000000
+0200
+++ new/testfixtures-12.2.0/docs/logging.rst 2026-06-20 11:53:06.000000000
+0200
@@ -127,6 +127,37 @@
... order_matters=False
... )
+Pass ``level`` to exclude entries below it, such as debugging output:
+
+>>> import logging
+>>> log.check(
+... ('INFO', 'start of block number 1'),
+... ('ERROR', 'error occurred'),
+... level=logging.INFO,
+... )
+
+Pass ``predicate`` to filter on each captured
:class:`~testfixtures.logcapture.Entry`,
+including its ``raw`` framework object; only those for which it returns true
are compared:
+
+>>> log.check(
+... ('ERROR', 'error occurred'),
+... predicate=lambda entry: entry.exception is not None,
+... )
+
+Both can be combined; entries excluded by either are left un-checked, so
+:meth:`~testfixtures.LogCapture.ensure_checked` still reports them.
+
+To assert that nothing was logged,
:meth:`~testfixtures.LogCapture.check_empty` is
+clearer than :meth:`~testfixtures.LogCapture.check` with no expected entries.
It
+takes the same ``level`` and ``predicate``, so you can assert that nothing was
+logged above a level:
+
+.. code-block:: python
+
+ with LogCapture() as quiet:
+ getLogger().debug('just debugging')
+ quiet.check_empty(level=logging.INFO)
+
Inspecting
~~~~~~~~~~
@@ -235,6 +266,18 @@
>>> logs.check(('INFO', 'something we care about'))
+To ignore noisy setup, use the :meth:`~testfixtures.LogCapture.disabled`
context
+manager, which uninstalls the capture for the duration of the ``with`` block:
+
+.. code-block:: python
+
+ with LogCapture(LoggingSource()) as logs:
+ with logs.disabled():
+ logger.warning('noisy setup output')
+ logger.info('what we care about')
+
+ logs.check(('INFO', 'what we care about'))
+
You can also capture different attributes by specifying their names; if the
attribute is
callable, as with ``getMessage`` here, it will be called:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/testfixtures-12.1.0/pyproject.toml
new/testfixtures-12.2.0/pyproject.toml
--- old/testfixtures-12.1.0/pyproject.toml 2026-06-15 17:22:45.000000000
+0200
+++ new/testfixtures-12.2.0/pyproject.toml 2026-06-20 11:53:06.000000000
+0200
@@ -1,6 +1,6 @@
[project]
name = "testfixtures"
-version = "12.1.0"
+version = "12.2.0"
description = "A collection of helpers and mock objects for unit tests and doc
tests."
readme = "README.rst"
authors = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/testfixtures-12.1.0/src/testfixtures/logcapture.py
new/testfixtures-12.2.0/src/testfixtures/logcapture.py
--- old/testfixtures-12.1.0/src/testfixtures/logcapture.py 2026-06-15
17:22:45.000000000 +0200
+++ new/testfixtures-12.2.0/src/testfixtures/logcapture.py 2026-06-20
11:53:06.000000000 +0200
@@ -2,12 +2,13 @@
import logging
import warnings
from collections import defaultdict
+from contextlib import contextmanager
from dataclasses import dataclass
from logging import LogRecord
from pprint import pformat
from types import TracebackType
from typing import (
- Any, Callable, Sequence, TypeAlias, TypeVar, List, Tuple, Self, Protocol,
overload
+ Any, Callable, Iterator, Sequence, TypeAlias, TypeVar, List, Tuple, Self,
Protocol, overload
)
from warnings import warn
@@ -191,6 +192,7 @@
names = (names,)
self._sources = [LoggingSource(attributes, level, names=names,
propagate=propagate)]
self.recursive_check = recursive_check
+ self._disabled = False
if ensure_checks_above is None:
self.ensure_checks_above = self.default_ensure_checks_above
else:
@@ -255,6 +257,20 @@
def _collect_entry(self, entry: Entry) -> None:
self.entries.append(entry)
+ @contextmanager
+ def disabled(self) -> Iterator[None]:
+ """
+ A context manager that stops capturing for the duration of the
``with`` block by
+ uninstalling the capture and reinstalling it on exit. Anything logged
while it is
+ active is handled as if the capture were not present, which is useful
for ignoring
+ noisy setup in a test.
+ """
+ self.uninstall()
+ try:
+ yield
+ finally:
+ self.install()
+
def install(self) -> Self | None:
"""
Install this :class:`LogCapture`, enabling all configured sources to
begin capturing.
@@ -298,7 +314,14 @@
tuples = (r if isinstance(r, tuple) else (r,) for r in self.actual())
return '\n'.join(' '.join(str(e) for e in t) for t in tuples)
- def check(self, *expected: Any, order_matters: bool = True, raises: bool =
True) -> str | None:
+ def check(
+ self,
+ *expected: Any,
+ order_matters: bool = True,
+ raises: bool = True,
+ level: int | None = None,
+ predicate: Callable[[Entry], bool] | None = None,
+ ) -> str | None:
"""
This will compare the captured entries with the expected
entries provided and raise an :class:`AssertionError` if they
@@ -317,16 +340,36 @@
:param raises: If ``False``, the message that would be raised in the
:class:`AssertionError` will be returned instead of the
exception being raised.
+
+ :param level:
+
+ A keyword-only parameter that, if provided, excludes any captured
entry
+ with a numeric :attr:`~testfixtures.logcapture.Entry.level` below it
from
+ the comparison. Entries with a ``level`` of ``None`` are always
included.
+
+ :param predicate:
+
+ A keyword-only parameter that, if provided, is called with each
captured
+ :class:`~testfixtures.logcapture.Entry`; only those for which it
returns
+ a true value are included in the comparison.
"""
__tracebackhide__ = True
+ actual = []
+ for entry in self.entries:
+ if level is not None and entry.level is not None and entry.level <
level:
+ continue
+ if predicate is not None and not predicate(entry):
+ continue
+ entry.checked = True
+ actual.append(entry.actual)
+
result = None
if order_matters:
result = compare(
- expected, actual=self.actual(),
recursive=self.recursive_check, raises=False
+ expected, actual=actual, recursive=self.recursive_check,
raises=False
)
else:
- actual = self.actual()
expected_ = SequenceComparison(
*expected, ordered=False, partial=False,
recursive=self.recursive_check
)
@@ -334,9 +377,32 @@
result = expected_.failed
if result and raises:
raise AssertionError(result)
- self.mark_all_checked()
return result
+ def check_empty(
+ self,
+ level: int | None = None,
+ predicate: Callable[[Entry], bool] | None = None,
+ ) -> None:
+ """
+ Assert that nothing was logged, raising an :class:`AssertionError` if
it was.
+ This is a clearer way of writing :meth:`check` with no expected
entries.
+
+ :param level:
+
+ If provided, only entries with a numeric
+ :attr:`~testfixtures.logcapture.Entry.level` at or above it are
considered.
+ Entries with a ``level`` of ``None`` are always considered.
+
+ :param predicate:
+
+ If provided, it is called with each captured
+ :class:`~testfixtures.logcapture.Entry`; only those for which it
returns
+ a true value are considered.
+ """
+ __tracebackhide__ = True
+ self.check(level=level, predicate=predicate)
+
def raise_first_exception(self, start_index: int = 0) -> None:
"""
Raise the first captured exception from ``start_index`` onwards.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/testfixtures-12.1.0/tests/test_logcapture.py
new/testfixtures-12.2.0/tests/test_logcapture.py
--- old/testfixtures-12.1.0/tests/test_logcapture.py 2026-06-15
17:22:45.000000000 +0200
+++ new/testfixtures-12.2.0/tests/test_logcapture.py 2026-06-20
11:53:06.000000000 +0200
@@ -1,10 +1,10 @@
import atexit
-from logging import getLogger, ERROR, Filter, shutdown
+from logging import getLogger, INFO, WARNING, ERROR, Filter, shutdown
from textwrap import dedent
from unittest import TestCase
-from testfixtures import Replacer, LogCapture, compare, Replace, ShouldWarn
-from testfixtures.logcapture import LoggingSource
+from testfixtures import Replacer, LogCapture, compare, Replace, ShouldRaise,
ShouldWarn
+from testfixtures.logcapture import Entry, LoggingSource
from testfixtures.mock import Mock, call
from testfixtures.shouldraise import ShouldAssert
@@ -19,6 +19,20 @@
return True
+class LevellessSource:
+ # A minimal CaptureSource for a framework that has no comparable numeric
+ # level, so its entries are captured with level=None.
+
+ def install(self, collector):
+ self.collector = collector
+
+ def uninstall(self):
+ self.collector = None
+
+ def log(self, *actual):
+ self.collector(Entry(raw=actual, actual=actual, level=None))
+
+
class TestLogCapture(TestCase):
def test_simple(self):
@@ -390,6 +404,137 @@
shutdown()
+class TestCheck:
+
+ def test_raises_false_no_difference(self):
+ with LogCapture() as log:
+ root.info('hello')
+ compare(log.check(('root', 'INFO', 'hello'), raises=False),
expected=None)
+
+ def test_raises_false_with_difference(self):
+ with LogCapture() as log:
+ root.info('hello')
+ compare(log.check(('root', 'INFO', 'world'), raises=False), expected=(
+ "sequence not as expected:\n\n"
+ "same:\n()\n\n"
+ "expected:\n(('root', 'INFO', 'world'),)\n\n"
+ "actual:\n(('root', 'INFO', 'hello'),)"
+ ))
+
+ def test_level(self):
+ with LogCapture() as log:
+ root.debug('noisy')
+ root.info('one')
+ root.error('two')
+ log.check(
+ ('root', 'INFO', 'one'),
+ ('root', 'ERROR', 'two'),
+ level=INFO,
+ )
+
+ def test_level_order_doesnt_matter(self):
+ with LogCapture() as log:
+ root.debug('noisy')
+ root.error('two')
+ root.info('one')
+ log.check(
+ ('root', 'INFO', 'one'),
+ ('root', 'ERROR', 'two'),
+ level=INFO,
+ order_matters=False,
+ )
+
+ def test_level_keeps_none_level(self):
+ source = LevellessSource()
+ with LogCapture(source) as log:
+ source.log('custom', 'a message')
+ log.check(('custom', 'a message'), level=ERROR)
+
+ def test_predicate(self):
+ with LogCapture() as log:
+ root.info('one')
+ root.error('two')
+ log.check(
+ ('root', 'ERROR', 'two'),
+ predicate=lambda entry: entry.level is not None and entry.level >=
ERROR,
+ )
+
+ def test_level_and_predicate_compose(self):
+ with LogCapture() as log:
+ root.debug('skip: too low')
+ root.info('keep')
+ root.warning('skip: predicate')
+ log.check(
+ ('root', 'INFO', 'keep'),
+ level=INFO,
+ predicate=lambda entry: 'skip' not in entry.actual[2],
+ )
+
+ def test_excluded_entries_not_marked_checked(self):
+ log_capture = LogCapture(ensure_checks_above=ERROR)
+ root.info('info')
+ root.error('boom')
+ log_capture.uninstall()
+ log_capture.check(
+ ('root', 'INFO', 'info'),
+ predicate=lambda entry: entry.level is not None and entry.level <
ERROR,
+ )
+ with ShouldAssert("Not asserted ERROR log(s): [('root', 'ERROR',
'boom')]"):
+ log_capture.ensure_checked()
+
+
+class TestCheckEmpty:
+
+ def test_empty(self):
+ with LogCapture() as log:
+ pass
+ log.check_empty()
+
+ def test_not_empty(self):
+ with LogCapture() as log:
+ root.info('boom')
+ with ShouldAssert(
+ "sequence not as expected:\n\n"
+ "same:\n()\n\n"
+ "expected:\n()\n\n"
+ "actual:\n(('root', 'INFO', 'boom'),)"
+ ):
+ log.check_empty()
+
+ def test_level_excludes_lower(self):
+ with LogCapture() as log:
+ root.debug('noisy')
+ root.info('chatter')
+ log.check_empty(level=WARNING)
+
+ def test_level_not_empty(self):
+ with LogCapture() as log:
+ root.debug('noisy')
+ root.error('boom')
+ with ShouldAssert(
+ "sequence not as expected:\n\n"
+ "same:\n()\n\n"
+ "expected:\n()\n\n"
+ "actual:\n(('root', 'ERROR', 'boom'),)"
+ ):
+ log.check_empty(level=WARNING)
+
+ def test_predicate_excludes_all(self):
+ with LogCapture() as log:
+ root.info('chatter')
+ root.warning('still fine')
+ log.check_empty(predicate=lambda entry: entry.level is not None and
entry.level >= ERROR)
+
+ def test_level_and_predicate_compose(self):
+ with LogCapture() as log:
+ root.debug('debug noise')
+ root.error('expected error')
+ log.check_empty(
+ level=WARNING,
+ predicate=lambda entry: 'expected' not in entry.actual[2],
+ )
+
+
class TestCheckPresent:
def test_order_matters_ok(self):
@@ -673,21 +818,6 @@
order_matters=False
)
- def test_check_raises_false_no_difference(self):
- with LogCapture() as log:
- root.info('hello')
- compare(log.check(('root', 'INFO', 'hello'), raises=False),
expected=None)
-
- def test_check_raises_false_with_difference(self):
- with LogCapture() as log:
- root.info('hello')
- compare(log.check(('root', 'INFO', 'world'), raises=False), expected=(
- "sequence not as expected:\n\n"
- "same:\n()\n\n"
- "expected:\n(('root', 'INFO', 'world'),)\n\n"
- "actual:\n(('root', 'INFO', 'hello'),)"
- ))
-
def test_check_present_raises_false_no_difference(self):
with LogCapture() as log:
root.info('hello')
@@ -702,3 +832,25 @@
"expected:\n[('root', 'INFO', 'world')]\n\n"
"actual:\n[]"
))
+
+
+class TestDisabled:
+
+ def test_disabled(self):
+ with LogCapture() as log:
+ root.info('before')
+ with log.disabled():
+ root.info('noise')
+ root.info('after')
+ log.check(
+ ('root', 'INFO', 'before'),
+ ('root', 'INFO', 'after'),
+ )
+
+ def test_disabled_restores_on_exception(self):
+ with LogCapture() as log:
+ with ShouldRaise(ValueError('boom')):
+ with log.disabled():
+ raise ValueError('boom')
+ root.info('after')
+ log.check(('root', 'INFO', 'after'))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/testfixtures-12.1.0/tests/test_loguru.py
new/testfixtures-12.2.0/tests/test_loguru.py
--- old/testfixtures-12.1.0/tests/test_loguru.py 2026-06-15
17:22:45.000000000 +0200
+++ new/testfixtures-12.2.0/tests/test_loguru.py 2026-06-20
11:53:06.000000000 +0200
@@ -99,6 +99,26 @@
logger.warning('captured')
log.check(('WARNING', 'captured'))
+ def test_check_level(self):
+ with LogCapture(LoguruSource()) as log:
+ logger.debug('noisy')
+ logger.info('useful')
+ logger.error('boom')
+ log.check(
+ ('INFO', 'useful'),
+ ('ERROR', 'boom'),
+ level=logging.INFO,
+ )
+
+ def test_check_predicate_on_raw(self):
+ with LogCapture(LoguruSource()) as log:
+ logger.bind(user='admin').warning('deleted a record')
+ logger.warning('cache expired')
+ log.check(
+ ('WARNING', 'deleted a record'),
+ predicate=lambda entry: 'user' in entry.raw['extra'],
+ )
+
def test_str(self):
with LogCapture(LoguruSource()) as log:
logger.info('hello world')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/testfixtures-12.1.0/tests/test_structlog.py
new/testfixtures-12.2.0/tests/test_structlog.py
--- old/testfixtures-12.1.0/tests/test_structlog.py 2026-06-15
17:22:45.000000000 +0200
+++ new/testfixtures-12.2.0/tests/test_structlog.py 2026-06-20
11:53:06.000000000 +0200
@@ -82,6 +82,28 @@
structlog.get_logger().warning('captured')
log.check(('WARNING', 'captured'))
+ def test_check_level(self):
+ logger = structlog.get_logger()
+ with LogCapture(StructlogSource()) as log:
+ logger.debug('noisy')
+ logger.info('useful')
+ logger.error('boom')
+ log.check(
+ ('INFO', 'useful'),
+ ('ERROR', 'boom'),
+ level=logging.INFO,
+ )
+
+ def test_check_predicate_on_raw(self):
+ logger = structlog.get_logger()
+ with LogCapture(StructlogSource()) as log:
+ logger.info('request handled', path='/api/users')
+ logger.info('request handled', path='/health')
+ log.check(
+ ('INFO', 'request handled'),
+ predicate=lambda entry: entry.raw.get('path') != '/health',
+ )
+
def test_broken_attribute_method(self):
def bad(event_dict):
raise ValueError('boom')