Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-psygnal for openSUSE:Factory 
checked in at 2025-05-20 09:32:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-psygnal (Old)
 and      /work/SRC/openSUSE:Factory/.python-psygnal.new.30101 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-psygnal"

Tue May 20 09:32:53 2025 rev:8 rq:1277832 version:0.13.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-psygnal/python-psygnal.changes    
2025-04-30 19:04:38.426971570 +0200
+++ /work/SRC/openSUSE:Factory/.python-psygnal.new.30101/python-psygnal.changes 
2025-05-20 09:35:00.837502815 +0200
@@ -1,0 +2,17 @@
+Thu May 15 08:05:13 UTC 2025 - John Paul Adrian Glaubitz 
<adrian.glaub...@suse.com>
+
+- Update to 0.13.0
+  * feat: add testing utilities (#368)
+  * fix: Don't use deprecated model_fields access (#364)
+  * build: fix building of wheels with uv (#370)
+  * ci(pre-commit.ci): autoupdate (#369)
+  * docs: general docs update, use mkdocs-api-autonav (#367)
+  * build: use pyproject dependency groups and uv (#366)
+  * ci(dependabot): bump pypa/cibuildwheel from 2.22 to 2.23 (#360)
+  * Add back universal (none-any) wheel (#358)
+  * ci(pre-commit.ci): autoupdate (#355)
+- Drop support-pydantic-211.patch, merged upstream
+- Update Suggests from pyproject.toml
+- Use Python 3.11 on SLE-15 by default
+
+-------------------------------------------------------------------

Old:
----
  psygnal-0.12.0.tar.gz
  support-pydantic-211.patch

New:
----
  psygnal-0.13.0.tar.gz

BETA DEBUG BEGIN:
  Old:  * ci(pre-commit.ci): autoupdate (#355)
- Drop support-pydantic-211.patch, merged upstream
- Update Suggests from pyproject.toml
BETA DEBUG END:

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-psygnal.spec ++++++
--- /var/tmp/diff_new_pack.HL211x/_old  2025-05-20 09:35:02.637578001 +0200
+++ /var/tmp/diff_new_pack.HL211x/_new  2025-05-20 09:35:02.645578335 +0200
@@ -16,15 +16,14 @@
 #
 
 
+%{?sle15_python_module_pythons}
 Name:           python-psygnal
-Version:        0.12.0
+Version:        0.13.0
 Release:        0
 Summary:        Fast python callback/event system modeled after Qt Signals
 License:        BSD-3-Clause
 URL:            https://github.com/pyapp-kit/psygnal
 Source:         
https://files.pythonhosted.org/packages/source/p/psygnal/psygnal-%{version}.tar.gz
-# PATCH-FIX-UPSTREAM gh#pyapp-kit/psygnal#364
-Patch0:         support-pydantic-211.patch
 BuildRequires:  %{python_module hatch-vcs}
 BuildRequires:  %{python_module hatchling >= 1.8.0}
 BuildRequires:  %{python_module pip}
@@ -46,7 +45,7 @@
 Suggests:       python-numpy
 Suggests:       python-pydantic
 Suggests:       python-qtpy
-Suggests:       python-rich
+Suggests:       python-rich >= 14.0.0
 Suggests:       python-wrapt
 Suggests:       python-griffe == 0.25.5
 Suggests:       python-wrapt

++++++ psygnal-0.12.0.tar.gz -> psygnal-0.13.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/.gitignore 
new/psygnal-0.13.0/.gitignore
--- old/psygnal-0.12.0/.gitignore       2025-02-03 16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/.gitignore       2025-05-06 00:09:46.000000000 +0200
@@ -113,3 +113,6 @@
 psygnal/_version.py
 .asv/
 wheelhouse/
+
+# for now...
+uv.lock  
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/CHANGELOG.md 
new/psygnal-0.13.0/CHANGELOG.md
--- old/psygnal-0.12.0/CHANGELOG.md     2025-02-03 16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/CHANGELOG.md     2025-05-06 00:09:46.000000000 +0200
@@ -1,5 +1,27 @@
 # Changelog
 
+## [v0.13.0](https://github.com/pyapp-kit/psygnal/tree/v0.13.0) (2025-05-05)
+
+[Full 
Changelog](https://github.com/pyapp-kit/psygnal/compare/v0.12.0...v0.13.0)
+
+**Implemented enhancements:**
+
+- feat: add testing utilities 
[\#368](https://github.com/pyapp-kit/psygnal/pull/368) 
([tlambert03](https://github.com/tlambert03))
+
+**Fixed bugs:**
+
+- fix: Don't use deprecated model\_fields access 
[\#364](https://github.com/pyapp-kit/psygnal/pull/364) 
([s-t-e-v-e-n-k](https://github.com/s-t-e-v-e-n-k))
+
+**Merged pull requests:**
+
+- build: fix building of wheels with uv 
[\#370](https://github.com/pyapp-kit/psygnal/pull/370) 
([tlambert03](https://github.com/tlambert03))
+- ci\(pre-commit.ci\): autoupdate 
[\#369](https://github.com/pyapp-kit/psygnal/pull/369) 
([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
+- docs: general docs update, use mkdocs-api-autonav 
[\#367](https://github.com/pyapp-kit/psygnal/pull/367) 
([tlambert03](https://github.com/tlambert03))
+- build: use pyproject dependency groups and uv 
[\#366](https://github.com/pyapp-kit/psygnal/pull/366) 
([tlambert03](https://github.com/tlambert03))
+- ci\(dependabot\): bump pypa/cibuildwheel from 2.22 to 2.23 
[\#360](https://github.com/pyapp-kit/psygnal/pull/360) 
([dependabot[bot]](https://github.com/apps/dependabot))
+- Add back universal \(none-any\) wheel 
[\#358](https://github.com/pyapp-kit/psygnal/pull/358) 
([tlambert03](https://github.com/tlambert03))
+- ci\(pre-commit.ci\): autoupdate 
[\#355](https://github.com/pyapp-kit/psygnal/pull/355) 
([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
+
 ## [v0.12.0](https://github.com/pyapp-kit/psygnal/tree/v0.12.0) (2025-02-03)
 
 [Full 
Changelog](https://github.com/pyapp-kit/psygnal/compare/v0.11.1...v0.12.0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/PKG-INFO new/psygnal-0.13.0/PKG-INFO
--- old/psygnal-0.12.0/PKG-INFO 2025-02-03 16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/PKG-INFO 2025-05-06 00:09:46.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: psygnal
-Version: 0.12.0
+Version: 0.13.0
 Summary: Fast python callback/event system modeled after Qt Signals
 Project-URL: homepage, https://github.com/pyapp-kit/psygnal
 Project-URL: repository, https://github.com/pyapp-kit/psygnal
@@ -19,54 +19,10 @@
 Classifier: Programming Language :: Python :: 3.13
 Classifier: Typing :: Typed
 Requires-Python: >=3.9
-Provides-Extra: dev
-Requires-Dist: attrs; extra == 'dev'
-Requires-Dist: dask[array]>=2024.0.0; extra == 'dev'
-Requires-Dist: ipython; extra == 'dev'
-Requires-Dist: msgspec; extra == 'dev'
-Requires-Dist: mypy; extra == 'dev'
-Requires-Dist: mypy-extensions; extra == 'dev'
-Requires-Dist: numpy>1.21.6; extra == 'dev'
-Requires-Dist: pre-commit; extra == 'dev'
-Requires-Dist: pydantic; extra == 'dev'
-Requires-Dist: pyinstaller>=4.0; extra == 'dev'
-Requires-Dist: pyqt6; extra == 'dev'
-Requires-Dist: pytest-cov; extra == 'dev'
-Requires-Dist: pytest-mypy-plugins; extra == 'dev'
-Requires-Dist: pytest-qt; extra == 'dev'
-Requires-Dist: pytest>=6.0; extra == 'dev'
-Requires-Dist: qtpy; extra == 'dev'
-Requires-Dist: rich; extra == 'dev'
-Requires-Dist: ruff; extra == 'dev'
-Requires-Dist: toolz; extra == 'dev'
-Requires-Dist: typing-extensions; extra == 'dev'
-Requires-Dist: wrapt; extra == 'dev'
-Provides-Extra: docs
-Requires-Dist: griffe==0.25.5; extra == 'docs'
-Requires-Dist: mkdocs-material==8.5.10; extra == 'docs'
-Requires-Dist: mkdocs-minify-plugin; extra == 'docs'
-Requires-Dist: mkdocs-spellcheck[all]; extra == 'docs'
-Requires-Dist: mkdocs==1.4.2; extra == 'docs'
-Requires-Dist: mkdocstrings-python==0.8.3; extra == 'docs'
-Requires-Dist: mkdocstrings==0.20.0; extra == 'docs'
 Provides-Extra: proxy
 Requires-Dist: wrapt; extra == 'proxy'
 Provides-Extra: pydantic
 Requires-Dist: pydantic; extra == 'pydantic'
-Provides-Extra: test
-Requires-Dist: attrs; extra == 'test'
-Requires-Dist: dask[array]>=2024.0.0; extra == 'test'
-Requires-Dist: msgspec; extra == 'test'
-Requires-Dist: numpy>1.21.6; extra == 'test'
-Requires-Dist: pydantic; extra == 'test'
-Requires-Dist: pyinstaller>=4.0; extra == 'test'
-Requires-Dist: pytest-cov; extra == 'test'
-Requires-Dist: pytest>=6.0; extra == 'test'
-Requires-Dist: toolz; extra == 'test'
-Requires-Dist: wrapt; extra == 'test'
-Provides-Extra: testqt
-Requires-Dist: pytest-qt; extra == 'testqt'
-Requires-Dist: qtpy; extra == 'testqt'
 Description-Content-Type: text/markdown
 
 # psygnal
@@ -192,16 +148,22 @@
 
 ## Developers
 
+### Setup
+
+This project uses PEP 735 dependency groups.
+
+After cloning, setup your env with `uv sync` or `pip install -e . --group dev`
+
 ### Compiling
 
 While `psygnal` is a pure python package, it is compiled with mypyc to increase
 performance.  To test the compiled version locally, you can run:
 
 ```bash
-make build
+HATCH_BUILD_HOOKS_ENABLE=1 uv sync --force-reinstall
 ```
 
-(which is just an alias for `HATCH_BUILD_HOOKS_ENABLE=1 pip install -e .`)
+(which is also available as `make build` if you have make installed)
 
 ### Debugging
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/README.md new/psygnal-0.13.0/README.md
--- old/psygnal-0.12.0/README.md        2025-02-03 16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/README.md        2025-05-06 00:09:46.000000000 +0200
@@ -121,16 +121,22 @@
 
 ## Developers
 
+### Setup
+
+This project uses PEP 735 dependency groups.
+
+After cloning, setup your env with `uv sync` or `pip install -e . --group dev`
+
 ### Compiling
 
 While `psygnal` is a pure python package, it is compiled with mypyc to increase
 performance.  To test the compiled version locally, you can run:
 
 ```bash
-make build
+HATCH_BUILD_HOOKS_ENABLE=1 uv sync --force-reinstall
 ```
 
-(which is just an alias for `HATCH_BUILD_HOOKS_ENABLE=1 pip install -e .`)
+(which is also available as `make build` if you have make installed)
 
 ### Debugging
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/pyproject.toml 
new/psygnal-0.13.0/pyproject.toml
--- old/psygnal-0.12.0/pyproject.toml   2025-02-03 16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/pyproject.toml   2025-05-06 00:09:46.000000000 +0200
@@ -32,17 +32,10 @@
 # extras
 # https://peps.python.org/pep-0621/#dependencies-optional-dependencies
 [project.optional-dependencies]
-docs = [
-    "griffe==0.25.5",
-    "mkdocs-material==8.5.10",
-    "mkdocs-minify-plugin",
-    "mkdocs==1.4.2",
-    "mkdocstrings-python==0.8.3",
-    "mkdocstrings==0.20.0",
-    "mkdocs-spellcheck[all]",
-]
 proxy = ["wrapt"]
 pydantic = ["pydantic"]
+
+[dependency-groups]
 test = [
     "dask[array]>=2024.0.0",
     "attrs",
@@ -55,19 +48,30 @@
     "msgspec",
     "toolz",
 ]
-testqt = ["pytest-qt", "qtpy"]
-
+testqt = [{ include-group = "test" }, "pytest-qt", "qtpy"]
+test-codspeed = [{ include-group = "test" }, "pytest-codspeed"]
+docs = [
+    "mkdocs-api-autonav",
+    "mkdocs-material",
+    "mkdocs-minify-plugin",
+    "mkdocs-spellcheck[all]",
+    "mkdocs",
+    "mkdocstrings-python",
+    "ruff",
+]
 dev = [
-    "psygnal[test, testqt]",
+    { include-group = "test" },
+    { include-group = "docs" },
     "PyQt6",
     "ipython",
     "mypy",
     "mypy_extensions",
     "pre-commit",
     "pytest-mypy-plugins",
-    "rich",
     "ruff",
     "typing-extensions",
+    "rich>=14.0.0",
+    "pdbpp; sys_platform != 'win32'",
 ]
 
 [project.urls]
@@ -116,9 +120,10 @@
 # Skip 32-bit builds & PyPy wheels on all platforms
 skip = ["*-manylinux_i686", "*-musllinux_i686", "*-win32", "pp*"]
 build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*"]
-test-extras = ["test"]
+test-groups = ["test"]
 test-command = "pytest {project}/tests -v"
 test-skip = ["*-musllinux*", "cp312-win*", "*-macosx_arm64"]
+build-frontend = "build[uv]"
 
 [[tool.cibuildwheel.overrides]]
 select = "*-manylinux_i686*"
@@ -127,6 +132,10 @@
 [tool.cibuildwheel.environment]
 HATCH_BUILD_HOOKS_ENABLE = "1"
 
+[tool.check-wheel-contents]
+# W004: Module is not located at importable path (hook-psygnal.py)
+ignore = ["W004"]
+
 # https://docs.astral.sh/ruff/
 [tool.ruff]
 line-length = 88
@@ -147,7 +156,7 @@
     "C4",   # flake8-comprehensions
     "B",    # flake8-bugbear
     "A001", # flake8-builtins
-    "TC",  # flake8-typecheck
+    "TC",   # flake8-typecheck
     "TID",  # flake8-tidy-imports
     "RUF",  # ruff-specific rules
 ]
@@ -167,6 +176,7 @@
 [tool.pytest.ini_options]
 minversion = "6.0"
 testpaths = ["tests"]
+addopts = ["--color=yes"]
 filterwarnings = [
     "error",
     "ignore:The distutils package is deprecated:DeprecationWarning:",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/src/psygnal/__init__.py 
new/psygnal-0.13.0/src/psygnal/__init__.py
--- old/psygnal-0.12.0/src/psygnal/__init__.py  2025-02-03 16:41:13.000000000 
+0100
+++ new/psygnal-0.13.0/src/psygnal/__init__.py  2025-05-06 00:09:46.000000000 
+0200
@@ -8,7 +8,7 @@
 from typing import TYPE_CHECKING, Any
 
 if TYPE_CHECKING:
-    from ._evented_model import EventedModel  # noqa: TC004
+    from ._evented_model import EventedModel
 
 
 try:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/src/psygnal/_evented_model.py 
new/psygnal-0.13.0/src/psygnal/_evented_model.py
--- old/psygnal-0.12.0/src/psygnal/_evented_model.py    2025-02-03 
16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/src/psygnal/_evented_model.py    2025-05-06 
00:09:46.000000000 +0200
@@ -111,7 +111,8 @@
     ) -> dict[str, Any]:
         """Get possibly nested default values for a Model object."""
         dflt = {}
-        for k, v in obj.model_fields.items():
+        cls = obj if isinstance(obj, type) else type(obj)
+        for k, v in cls.model_fields.items():
             d = v.get_default()
             if (
                 d is None
@@ -125,7 +126,9 @@
     def _get_config(cls: pydantic.BaseModel) -> "ConfigDict":
         return cls.model_config
 
-    def _get_fields(cls: pydantic.BaseModel) -> dict[str, 
pydantic.fields.FieldInfo]:
+    def _get_fields(
+        cls: type[pydantic.BaseModel],
+    ) -> dict[str, pydantic.fields.FieldInfo]:
         return cls.model_fields
 
     def _model_dump(obj: pydantic.BaseModel) -> dict:
@@ -384,7 +387,7 @@
     (see [pydantic docs](https://pydantic-docs.helpmanual.io/usage/models/)),
     this class adds the following:
 
-    1. gains an `events` attribute that is an instance of 
[`psygnal.SignalGroup`][].
+    1. Gains an `events` attribute that is an instance of 
[`psygnal.SignalGroup`][].
        This group will have a signal for each field in the model (excluding 
private
        attributes and non-mutable fields).  Whenever a field in the model is 
mutated,
        the corresponding signal will emit with the new value (see example 
below).
@@ -404,14 +407,16 @@
           dependencies by inspecting the source code of the property getter 
for.
 
     4. If you would like to allow custom fields to provide their own 
json_encoders, you
-       can either use the standard pydantic method of adding json_encoders to 
your
-       model, for each field type you'd like to support:
-       
https://pydantic-docs.helpmanual.io/usage/exporting_models/#json_encoders
-       This `EventedModel` class will additionally look for a `_json_encode` 
method
-       on any field types in the model.  If a field type declares a 
`_json_encode`
-       method, it will be added to the
-       
[`json_encoders`](https://pydantic-docs.helpmanual.io/usage/exporting_models/#json_encoders)
-       dict in the model `Config`.
+       can either:
+
+        1. use the [standard pydantic
+        method](https://pydantic-docs.helpmanual.io/usage/exporting_models) of 
adding
+        json_encoders to your model, for each field type you'd like to 
support: 1. This
+        `EventedModel` class will additionally look for a `_json_encode` 
method on any
+        field types in the model.  If a field type declares a `_json_encode` 
method, it
+        will be added to the
+        
[`json_encoders`](https://pydantic-docs.helpmanual.io/usage/exporting_models/#json_encoders)
+        dict in the model `Config`.  (Prefer using the standard pydantic 
method)
 
     Examples
     --------
@@ -547,7 +552,7 @@
     def reset(self) -> None:
         """Reset the state of the model to default values."""
         model_config = _get_config(self)
-        model_fields = _get_fields(self)
+        model_fields = _get_fields(type(self))
         for name, value in self._defaults.items():
             if isinstance(value, EventedModel):
                 cast("EventedModel", getattr(self, name)).reset()
@@ -710,6 +715,6 @@
                 yield
             finally:
                 if before is not NULL:  # pragma: no cover
-                    cls.model_config["use_enum_values"] = cast(bool, before)
+                    cls.model_config["use_enum_values"] = cast("bool", before)
                 else:
                     cls.model_config.pop("use_enum_values")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/src/psygnal/_group_descriptor.py 
new/psygnal-0.13.0/src/psygnal/_group_descriptor.py
--- old/psygnal-0.12.0/src/psygnal/_group_descriptor.py 2025-02-03 
16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/src/psygnal/_group_descriptor.py 2025-05-06 
00:09:46.000000000 +0200
@@ -52,7 +52,7 @@
     # if the class has an __eq_operators__ attribute, we use it
     # otherwise use/create the entry for `cls` in the global _EQ_OPERATORS map
     if hasattr(cls, _EQ_OPERATOR_NAME):
-        return cast(dict, getattr(cls, _EQ_OPERATOR_NAME))
+        return cast("dict", getattr(cls, _EQ_OPERATOR_NAME))
     else:
         return _EQ_OPERATORS.setdefault(cls, {})
 
@@ -389,7 +389,7 @@
             if name == signal_group_name:
                 return super_setattr(self, name, value)
 
-            group = cast(SignalGroup, getattr(self, signal_group_name))
+            group = cast("SignalGroup", getattr(self, signal_group_name))
             if not with_aliases and name not in group:
                 return super_setattr(self, name, value)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/src/psygnal/_weak_callback.py 
new/psygnal-0.13.0/src/psygnal/_weak_callback.py
--- old/psygnal-0.12.0/src/psygnal/_weak_callback.py    2025-02-03 
16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/src/psygnal/_weak_callback.py    2025-05-06 
00:09:46.000000000 +0200
@@ -458,7 +458,7 @@
         func = self._func_ref()
         if obj is None or func is None:
             return None
-        method = cast(MethodType, func.__get__(obj))
+        method = cast("MethodType", func.__get__(obj))
         if self._args or self._kwargs:
             return partial(method, *self._args, **self._kwargs)
         return method
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/src/psygnal/containers/__init__.py 
new/psygnal-0.13.0/src/psygnal/containers/__init__.py
--- old/psygnal-0.12.0/src/psygnal/containers/__init__.py       2025-02-03 
16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/src/psygnal/containers/__init__.py       2025-05-06 
00:09:46.000000000 +0200
@@ -1,30 +1,47 @@
-"""Containers backed by psygnal events."""
+"""Containers backed by psygnal events.
+
+These classes provide "evented" versions of mutable python containers.
+They each have an `events` attribute (`SignalGroup`) that has a variety of
+signals that will emit whenever the container is mutated.  See
+[Container SignalGroups](#container-signalgroups) for the corresponding
+container type for details on the available signals.
+"""
 
 from typing import TYPE_CHECKING, Any
 
-from ._evented_dict import EventedDict
-from ._evented_list import EventedList
-from ._evented_set import EventedOrderedSet, EventedSet, OrderedSet
+from ._evented_dict import DictEvents, EventedDict
+from ._evented_list import EventedList, ListEvents
+from ._evented_set import EventedOrderedSet, EventedSet, OrderedSet, SetEvents
 from ._selectable_evented_list import SelectableEventedList
 from ._selection import Selection
 
 if TYPE_CHECKING:
-    from ._evented_proxy import EventedCallableObjectProxy, EventedObjectProxy 
 # noqa
+    from ._evented_proxy import (
+        CallableProxyEvents,
+        EventedCallableObjectProxy,
+        EventedObjectProxy,
+        ProxyEvents,
+    )
 
 __all__ = [
+    "CallableProxyEvents",
+    "DictEvents",
     "EventedCallableObjectProxy",
     "EventedDict",
     "EventedList",
     "EventedObjectProxy",
     "EventedOrderedSet",
     "EventedSet",
+    "ListEvents",
     "OrderedSet",
+    "ProxyEvents",
     "SelectableEventedList",
     "Selection",
+    "SetEvents",
 ]
 
 
-def __getattr__(name: str) -> Any:
+def __getattr__(name: str) -> Any:  # pragma: no cover
     if name == "EventedObjectProxy":
         from ._evented_proxy import EventedObjectProxy
 
@@ -33,6 +50,14 @@
         from ._evented_proxy import EventedCallableObjectProxy
 
         return EventedCallableObjectProxy
+    if name == "CallableProxyEvents":
+        from ._evented_proxy import CallableProxyEvents
+
+        return CallableProxyEvents
+    if name == "ProxyEvents":
+        from ._evented_proxy import ProxyEvents
+
+        return ProxyEvents
     raise AttributeError(  # pragma: no cover
         f"module {__name__!r} has no attribute {name!r}"
     )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/psygnal-0.12.0/src/psygnal/containers/_evented_dict.py 
new/psygnal-0.13.0/src/psygnal/containers/_evented_dict.py
--- old/psygnal-0.12.0/src/psygnal/containers/_evented_dict.py  2025-02-03 
16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/src/psygnal/containers/_evented_dict.py  2025-05-06 
00:09:46.000000000 +0200
@@ -111,32 +111,22 @@
 
 
 class DictEvents(SignalGroup):
-    """Events available on [EventedDict][psygnal.containers.EventedDict].
-
-    Attributes
-    ----------
-    adding: Signal[Any]
-        `(key,)` emitted before an item is added at `key`
-    added : Signal[Any, Any]
-        `(key, value)` emitted after a `value` is added at `key`
-    changing : Signal[Any, Any, Any]
-        `(key, old_value, new_value)` emitted before `old_value` is replaced 
with
-        `new_value` at `key`
-    changed : Signal[Any, Any, Any]
-        `(key, old_value, new_value)` emitted before `old_value` is replaced 
with
-        `new_value` at `key`
-    removing: Signal[Any]
-        `(key,)` emitted before an item is removed at `key`
-    removed : Signal[Any, Any]
-        `(key, value)` emitted after `value` is removed at `index`
-    """
+    """Events available on [EventedDict][psygnal.containers.EventedDict]."""
 
     adding = Signal(object)  # (key, )
+    """`(key,)` emitted before an item is added at `key`"""
     added = Signal(object, object)  # (key, value)
+    """`(key, value)` emitted after a `value` is added at `key`"""
     changing = Signal(object)  # (key, )
+    """`(key, old_value, new_value)` emitted before `old_value` is replaced 
with
+    `new_value` at `key`"""
     changed = Signal(object, object, object)  # (key, old_value, value)
+    """`(key, old_value, new_value)` emitted before `old_value` is replaced 
with
+    `new_value` at `key`"""
     removing = Signal(object)  # (key, )
+    """`(key,)` emitted before an item is removed at `key`"""
     removed = Signal(object, object)  # (key, value)
+    """`(key, value)` emitted after `value` is removed at `key`"""
 
 
 class EventedDict(TypedMutableMapping[_K, _V]):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/psygnal-0.12.0/src/psygnal/containers/_evented_list.py 
new/psygnal-0.13.0/src/psygnal/containers/_evented_list.py
--- old/psygnal-0.12.0/src/psygnal/containers/_evented_list.py  2025-02-03 
16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/src/psygnal/containers/_evented_list.py  2025-05-06 
00:09:46.000000000 +0200
@@ -48,43 +48,32 @@
 
 
 class ListEvents(SignalGroup):
-    """Events available on [EventedList][psygnal.containers.EventedList].
-
-    Attributes
-    ----------
-    inserting : Signal[int]
-        `(index)` emitted before an item is inserted at `index`
-    inserted : Signal[int, Any]
-        `(index, value)` emitted after `value` is inserted at `index`
-    removing : Signal[int]
-        `(index)` emitted before an item is removed at `index`
-    removed: Signal[int, Any]
-        `(index, value)` emitted after `value` is removed at `index`
-    moving : Signal[int, int]
-        `(index, new_index)` emitted before an item is moved from `index` to 
`new_index`
-    moved : Signal[int, int, Any]
-        `(index, new_index, value)` emitted after `value` is moved from 
`index` to
-        `new_index`
-    changed : Signal[Union[int, slice], Any, Any]
-        `(index_or_slice, old_value, value)` emitted when `index` is set from
-        `old_value` to `value`
-    reordered : Signal
-        emitted when the list is reordered (eg. moved/reversed).
-    child_event : Signal[int, Any, SignalInstance, tuple]
-        `(index, object, emitter, args)` emitted when an object in the list 
emits an
-        event. Note that the `EventedList` must be created with 
`child_events=True` in
-        order for this to be emitted.
-    """
+    """Events available on [EventedList][psygnal.containers.EventedList]."""
 
     inserting = Signal(int)  # idx
+    """`(index)` emitted before an item is inserted at `index`"""
     inserted = Signal(int, object)  # (idx, value)
+    """`(index, value)` emitted after `value` is inserted at `index`"""
     removing = Signal(int)  # idx
+    """`(index)` emitted before an item is removed at `index`"""
     removed = Signal(int, object)  # (idx, value)
+    """`(index, value)` emitted after `value` is removed at `index`"""
     moving = Signal(int, int)  # (src_idx, dest_idx)
+    """`(index, new_index)` emitted before an item is moved from `index` to
+    `new_index`"""
     moved = Signal(int, int, object)  # (src_idx, dest_idx, value)
+    """`(index, new_index, value)` emitted after `value` is moved from
+    `index` to `new_index`"""
     changed = Signal(object, object, object)  # (int | slice, old, new)
+    """`(index_or_slice, old_value, value)` emitted when `index` is set from
+    `old_value` to `value`"""
     reordered = Signal()
+    """Emitted when the list is reordered (eg. moved/reversed)."""
     child_event = Signal(int, object, SignalInstance, tuple)
+    """`(index, object, emitter, args)` emitted when an object in the list 
emits an
+    event. Note that the `EventedList` must be created with 
`child_events=True` in
+    order for this to be emitted.
+    """
 
 
 class EventedList(MutableSequence[_T]):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/psygnal-0.12.0/src/psygnal/containers/_evented_proxy.py 
new/psygnal-0.13.0/src/psygnal/containers/_evented_proxy.py
--- old/psygnal-0.12.0/src/psygnal/containers/_evented_proxy.py 2025-02-03 
16:41:13.000000000 +0100
+++ new/psygnal-0.13.0/src/psygnal/containers/_evented_proxy.py 2025-05-06 
00:09:46.000000000 +0200
@@ -17,19 +17,25 @@
 
 
 class ProxyEvents(SignalGroup):
-    """ObjectProxy events."""
+    """Events emitted by `EventedObjectProxy` and 
`EventedCallableObjectProxy`."""
 
     attribute_set = Signal(str, object)
+    """Emitted when an attribute is set."""
     attribute_deleted = Signal(str)
+    """Emitted when an attribute is deleted."""
     item_set = Signal(object, object)
+    """Emitted when an item is set."""
     item_deleted = Signal(object)
+    """Emitted when an item is deleted."""
     in_place = Signal(str, object)
+    """Emitted when an in-place operation is performed."""
 
 
 class CallableProxyEvents(ProxyEvents):
-    """CallableObjectProxy events."""
+    """Events emitted by `EventedCallableObjectProxy`."""
 
     called = Signal(tuple, dict)
+    """Emitted when the object is called."""
 
 
 # we're using a cache instead of setting the events object directly on the 
proxy
@@ -41,6 +47,9 @@
 class EventedObjectProxy(ObjectProxy, Generic[T]):
     """Create a proxy of `target` that includes an `events` 
[psygnal.SignalGroup][].
 
+    Provides an "evented" subclasses of
+    
[`wrapt.ObjectProxy`](https://wrapt.readthedocs.io/en/latest/wrappers.html#object-proxy)
+
     !!! important
 
         This class requires `wrapt` to be installed.
@@ -61,6 +70,13 @@
     - `item_deleted`: `Signal(object)`
     - `in_place`: `Signal(str, object)`
 
+    !!! warning "Experimental"
+
+        This object is experimental! They may affect the behavior of
+        the wrapped object in unanticipated ways.  Please consult
+        the [wrapt 
documentation](https://wrapt.readthedocs.io/en/latest/wrappers.html)
+        for details on how the Object Proxy works.
+
     Parameters
     ----------
     target : Any
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/src/psygnal/testing.py 
new/psygnal-0.13.0/src/psygnal/testing.py
--- old/psygnal-0.12.0/src/psygnal/testing.py   1970-01-01 01:00:00.000000000 
+0100
+++ new/psygnal-0.13.0/src/psygnal/testing.py   2025-05-06 00:09:46.000000000 
+0200
@@ -0,0 +1,329 @@
+"""Utilities for testing psygnal Signals."""
+
+from __future__ import annotations
+
+from contextlib import contextmanager
+from typing import TYPE_CHECKING
+from unittest.mock import Mock
+from unittest.util import safe_repr
+
+if TYPE_CHECKING:
+    from collections.abc import Iterator
+    from typing import Any
+
+    from typing_extensions import Self
+
+    import psygnal
+
+__all__ = [
+    "SignalTester",
+    "assert_emitted",
+    "assert_emitted_once",
+    "assert_emitted_once_with",
+    "assert_emitted_with",
+    "assert_ever_emitted_with",
+    "assert_not_emitted",
+]
+
+
+class SignalTester:
+    """A tester object that listens to a signal and records its emissions.
+
+    This class wraps a [`psygnal.SignalInstance`][] and a 
[`unittest.mock.Mock`][]
+    object. It provides methods to connect and disconnect the mock from the 
signal, and
+    to assert that the signal was emitted with the expected arguments.  It 
also behaves
+    as a **context manager**, so you can monitor emissions of a signal within 
a specific
+    context.
+
+    !!! important
+
+        The signal is *not* automatically connected to the mock when the 
SignalTester is
+        created. You must call 
[`connect()`][psygnal.testing.SignalTester.connect] or
+        use the context manager to connect the mock to the signal.
+
+    Parameters
+    ----------
+    signal_instance : psygnal.SignalInstance
+        The signal instance to test.
+
+    Attributes
+    ----------
+    signal_instance : psygnal.SignalInstance
+        The signal instance to test.
+    mock : unittest.mock.Mock
+        The mock object that will be connected to the signal.
+
+    Examples
+    --------
+    ```python
+    from psygnal import Signal
+    from psygnal.testing import SignalTester
+
+
+    class MyObject:
+        value_changed = Signal(int)
+
+
+    obj = MyObject()
+    tester = SignalTester(obj.value_changed)
+    tester.assert_not_emitted()
+
+    with tester:
+        obj.value_changed.emit(1)
+
+    tester.assert_emitted()
+    tester.assert_emitted_once()
+    tester.assert_emitted_once_with(1)
+    assert tester.emit_count == 1
+    tester.reset()
+    assert tester.emit_count == 0
+    ```
+    """
+
+    def __init__(self, signal_instance: psygnal.SignalInstance) -> None:
+        super().__init__()
+        self.mock = Mock()
+        self.signal_instance = signal_instance
+
+    def reset(self) -> None:
+        """Reset the underlying mock object."""
+        self.mock.reset_mock()
+
+    @property
+    def emit_count(self) -> int:
+        """Return the number of times the signal was emitted."""
+        return self.mock.call_count
+
+    @property
+    def emit_args(self) -> tuple[Any, ...]:
+        """Return the arguments of the last emission of the signal."""
+        if (call_args := self.mock.call_args) is None:
+            return ()
+
+        return call_args[0]  # type: ignore[no-any-return]
+
+    @property
+    def emit_args_list(self) -> list[tuple[Any, ...]]:
+        """Return the arguments of all emissions of the signal."""
+        return [call[0] for call in self.mock.call_args_list]
+
+    def connect(self) -> None:
+        """Connect the mock to the signal."""
+        self.signal_instance.connect(self.mock)
+
+    def disconnect(self) -> None:
+        """Disconnect the mock from the signal."""
+        self.signal_instance.disconnect(self.mock)
+
+    def __enter__(self) -> Self:
+        """Connect the mock to the signal."""
+        self.connect()
+        return self
+
+    def __exit__(self, *args: Any) -> None:
+        """Disconnect the mock from the signal."""
+        self.disconnect()
+
+    @property
+    def signal_name(self) -> str:
+        """Return the name of the signal."""
+        return self.signal_instance.name or "signal"
+
+    def assert_not_emitted(self) -> None:
+        """Assert that the signal was never emitted."""
+        if self.mock.call_count != 0:
+            if self.mock.call_count == 1:
+                n = "once"
+            else:
+                n = f"{self.mock.call_count} times"
+            raise AssertionError(
+                f"Expected {self.signal_name!r} to not have been emitted. 
Emitted {n}."
+            )
+
+    def assert_emitted(self) -> None:
+        """Assert that the signal was emitted at least once."""
+        if self.mock.call_count == 0:
+            raise AssertionError(f"Expected {self.signal_name!r} to have been 
emitted.")
+
+    def assert_emitted_once(self) -> None:
+        """Assert that the signal was emitted exactly once."""
+        if not self.mock.call_count == 1:
+            raise AssertionError(
+                f"Expected {self.signal_name!r} to have been emitted once. "
+                f"Emitted {self.mock.call_count} times."
+            )
+
+    def assert_emitted_with(self, /, *args: Any) -> None:
+        """Assert that the *last* emission of the signal had the given 
arguments."""
+        if self.mock.call_args is None:
+            raise AssertionError(
+                f"Expected {self.signal_name!r} to have been emitted with 
arguments "
+                f"{args!r}.\nActual: not emitted"
+            )
+
+        actual = self.mock.call_args[0]
+        if actual != args:
+            raise AssertionError(
+                f"Expected {self.signal_name!r} to have been emitted with 
arguments "
+                f"{args!r}.\nActual: {actual}"
+            )
+
+    def assert_emitted_once_with(self, /, *args: Any) -> None:
+        """Assert that the signal was emitted exactly once with the given 
arguments."""
+        if not self.mock.call_count == 1:
+            raise AssertionError(
+                f"Expected {self.signal_name!r} to have been emitted exactly 
once. "
+                f"Emitted {self.mock.call_count} times."
+            )
+
+        actual = self.mock.call_args[0]
+        if actual != args:
+            raise AssertionError(
+                f"Expected {self.signal_name!r} to have been emitted once with 
"
+                f"arguments {args!r}.\nActual: {safe_repr(actual)}"
+            )
+
+    def assert_ever_emitted_with(self, /, *args: Any) -> None:
+        """Assert that the signal was emitted *ever* with the given 
arguments."""
+        if self.mock.call_args is None:
+            raise AssertionError(
+                f"Expected {self.signal_name!r} to have been emitted at least 
once "
+                f"with arguments {args!r}.\nActual: not emitted"
+            )
+
+        actual = [call[0] for call in self.mock.call_args_list]
+        if not any(call == args for call in actual):
+            _actual: tuple | list = actual[0] if len(actual) == 1 else actual
+            raise AssertionError(
+                f"Expected {self.signal_name!r} to have been emitted at least 
once "
+                f"with arguments {args!r}.\nActual: {safe_repr(_actual)}"
+            )
+
+
+@contextmanager
+def assert_emitted(signal: psygnal.SignalInstance) -> Iterator[SignalTester]:
+    """Assert that a signal was emitted at least once.
+
+    Parameters
+    ----------
+    signal : psygnal.SignalInstance
+        The signal instance to test.
+
+    Raises
+    ------
+    AssertionError
+        If the signal was never emitted.
+    """
+    with SignalTester(signal) as mock:
+        yield mock
+        mock.assert_emitted()
+
+
+@contextmanager
+def assert_emitted_once(signal: psygnal.SignalInstance) -> 
Iterator[SignalTester]:
+    """Assert that a signal was emitted exactly once.
+
+    Parameters
+    ----------
+    signal : psygnal.SignalInstance
+        The signal instance to test.
+
+    Raises
+    ------
+    AssertionError
+        If the signal was emitted more than once.
+    """
+    with SignalTester(signal) as mock:
+        yield mock
+        mock.assert_emitted_once()
+
+
+@contextmanager
+def assert_not_emitted(signal: psygnal.SignalInstance) -> 
Iterator[SignalTester]:
+    """Assert that a signal was never emitted.
+
+    Parameters
+    ----------
+    signal : psygnal.SignalInstance
+        The signal instance to test.
+
+    Raises
+    ------
+    AssertionError
+        If the signal was emitted at least once.
+    """
+    with SignalTester(signal) as mock:
+        yield mock
+        mock.assert_not_emitted()
+
+
+@contextmanager
+def assert_emitted_with(
+    signal: psygnal.SignalInstance, *args: Any
+) -> Iterator[SignalTester]:
+    """Assert that the *last* emission of the signal had the given arguments.
+
+    Parameters
+    ----------
+    signal : psygnal.SignalInstance
+        The signal instance to test.
+    args : Any
+        The arguments to check for in the last emission of the signal.
+
+    Raises
+    ------
+    AssertionError
+        If the signal was never emitted or if the last emission did not have 
the
+        expected arguments.
+    """
+    with assert_emitted(signal) as mock:
+        yield mock
+        mock.assert_emitted_with(*args)
+
+
+@contextmanager
+def assert_emitted_once_with(
+    signal: psygnal.SignalInstance, *args: Any
+) -> Iterator[SignalTester]:
+    """Assert that the signal was emitted exactly once with the given 
arguments.
+
+    Parameters
+    ----------
+    signal : psygnal.SignalInstance
+        The signal instance to test.
+    args : Any
+        The arguments to check for in the last emission of the signal.
+
+    Raises
+    ------
+    AssertionError
+        If the signal was not emitted or was emitted more than once or if the 
last
+        emission did not have the expected arguments.
+    """
+    with assert_emitted_once(signal) as mock:
+        yield mock
+        mock.assert_emitted_once_with(*args)
+
+
+@contextmanager
+def assert_ever_emitted_with(
+    signal: psygnal.SignalInstance, *args: Any
+) -> Iterator[SignalTester]:
+    """Assert that the signal was emitted *ever* with the given arguments.
+
+    Parameters
+    ----------
+    signal : psygnal.SignalInstance
+        The signal instance to test.
+    args : Any
+        The arguments to check for in any emission of the signal.
+
+    Raises
+    ------
+    AssertionError
+        If the signal was never emitted or if it was emitted but not with the 
expected
+        arguments.
+    """
+    with assert_emitted(signal) as mock:
+        yield mock
+        mock.assert_ever_emitted_with(*args)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.12.0/tests/test_testing_utils.py 
new/psygnal-0.13.0/tests/test_testing_utils.py
--- old/psygnal-0.12.0/tests/test_testing_utils.py      1970-01-01 
01:00:00.000000000 +0100
+++ new/psygnal-0.13.0/tests/test_testing_utils.py      2025-05-06 
00:09:46.000000000 +0200
@@ -0,0 +1,201 @@
+import re
+
+import pytest
+
+import psygnal.testing as pt
+from psygnal import Signal
+
+
+class MyObject:
+    changed = Signal()
+    value_changed = Signal(int)
+
+
+def test_assert_emitted() -> None:
+    obj = MyObject()
+
+    with pt.assert_emitted(obj.changed) as tester:
+        obj.changed.emit()
+
+    assert isinstance(tester, pt.SignalTester)
+
+    with pytest.raises(
+        AssertionError, match="Expected 'changed' to have been emitted."
+    ):
+        with pt.assert_emitted(obj.changed):
+            pass
+
+
+def test_assert_emitted_once():
+    obj = MyObject()
+    with pt.assert_emitted_once(obj.changed) as tester:
+        obj.changed.emit()
+    assert isinstance(tester, pt.SignalTester)
+
+    with pytest.raises(
+        AssertionError,
+        match="Expected 'changed' to have been emitted once. Emitted 2 times.",
+    ):
+        with pt.assert_emitted_once(obj.changed):
+            obj.changed.emit()
+            obj.changed.emit()
+
+
+def test_assert_not_emitted() -> None:
+    obj = MyObject()
+    with pt.assert_not_emitted(obj.changed) as tester:
+        pass
+
+    assert isinstance(tester, pt.SignalTester)
+
+    with pytest.raises(
+        AssertionError,
+        match="Expected 'changed' to not have been emitted. Emitted once.",
+    ):
+        with pt.assert_not_emitted(obj.changed):
+            obj.changed.emit()
+
+    with pytest.raises(
+        AssertionError,
+        match="Expected 'changed' to not have been emitted. Emitted 4 times.",
+    ):
+        with pt.assert_not_emitted(obj.changed):
+            obj.changed.emit()
+            obj.changed.emit()
+            obj.changed.emit()
+            obj.changed.emit()
+
+
+def test_assert_emitted_with() -> None:
+    obj = MyObject()
+    with pt.assert_emitted_with(obj.value_changed, 42) as tester:
+        obj.value_changed.emit(41)
+        obj.value_changed.emit(42)
+
+    assert isinstance(tester, pt.SignalTester)
+
+    with pytest.raises(
+        AssertionError,
+        match=re.escape(
+            "Expected 'value_changed' to have been emitted with arguments 
(42,)."
+            "\nActual: not emitted"
+        ),
+    ):
+        with pt.assert_emitted_with(obj.value_changed, 42):
+            pass
+
+    with pytest.raises(
+        AssertionError,
+        match=re.escape(
+            "Expected 'value_changed' to have been emitted with arguments 
(42,)."
+            "\nActual: (43,)"
+        ),
+    ):
+        with pt.assert_emitted_with(obj.value_changed, 42):
+            obj.value_changed.emit(42)
+            obj.value_changed.emit(43)
+
+
+def test_assert_emitted_once_with() -> None:
+    obj = MyObject()
+    with pt.assert_emitted_once_with(obj.value_changed, 42) as tester:
+        obj.value_changed.emit(42)
+
+    assert isinstance(tester, pt.SignalTester)
+
+    with pytest.raises(
+        AssertionError,
+        match=re.escape(
+            "Expected 'value_changed' to have been emitted exactly once. "
+            "Emitted 2 times."
+        ),
+    ):
+        with pt.assert_emitted_once_with(obj.value_changed, 42):
+            obj.value_changed.emit(42)
+            obj.value_changed.emit(42)
+
+    with pytest.raises(
+        AssertionError,
+        match=re.escape(
+            "Expected 'value_changed' to have been emitted once with arguments 
(42,)."
+            "\nActual: (43,)"
+        ),
+    ):
+        with pt.assert_emitted_once_with(obj.value_changed, 42):
+            obj.value_changed.emit(43)
+
+
+def test_assert_ever_emitted_with() -> None:
+    obj = MyObject()
+
+    with pt.assert_ever_emitted_with(obj.value_changed, 42) as tester:
+        obj.value_changed.emit(41)
+        obj.value_changed.emit(42)
+        obj.value_changed.emit(43)
+
+    assert isinstance(tester, pt.SignalTester)
+
+    with pytest.raises(
+        AssertionError,
+        match=re.escape(
+            "Expected 'value_changed' to have been emitted at least once with "
+            "arguments (42,)."
+            "\nActual: not emitted"
+        ),
+    ):
+        with pt.assert_ever_emitted_with(obj.value_changed, 42):
+            pass
+
+    with pytest.raises(
+        AssertionError,
+        match=re.escape(
+            "Expected 'value_changed' to have been emitted at least once with "
+            "arguments (42,)."
+            "\nActual: (43,)"
+        ),
+    ):
+        with pt.assert_ever_emitted_with(obj.value_changed, 42):
+            obj.value_changed.emit(43)
+
+    with pytest.raises(
+        AssertionError,
+        match=re.escape(
+            "Expected 'value_changed' to have been emitted at least once with "
+            "arguments (42,)."
+            "\nActual: [(41,), (42, 43)]"
+        ),
+    ):
+        with pt.assert_ever_emitted_with(obj.value_changed, 42):
+            obj.value_changed.emit(41)
+            obj.value_changed.emit(42, 43)
+
+
+def test_signal_tester() -> None:
+    obj = MyObject()
+    tester = pt.SignalTester(obj.changed)
+    tester.connect()
+    assert tester.signal_name == "changed"
+    assert tester.mock.call_count == 0
+
+    obj.changed.emit()
+
+    tester.assert_emitted_once()
+    tester.assert_emitted()
+    tester.assert_emitted_with()
+    assert tester.emit_count == 1
+    tester.reset()
+    assert tester.emit_count == 0
+
+    tester2 = pt.SignalTester(obj.value_changed)
+
+    with tester2:
+        obj.value_changed.emit(42)
+        obj.value_changed.emit(43)
+
+    tester2.assert_emitted()
+    tester2.assert_emitted_with(43)
+    tester2.assert_ever_emitted_with(42)
+
+    assert tester2.emit_args_list == [(42,), (43,)]
+    assert tester2.emit_count == 2
+    assert tester2.emit_args == (43,)

Reply via email to