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 2023-12-15 21:48:26
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-psygnal (Old)
 and      /work/SRC/openSUSE:Factory/.python-psygnal.new.25432 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-psygnal"

Fri Dec 15 21:48:26 2023 rev:2 rq:1133196 version:0.9.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-psygnal/python-psygnal.changes    
2023-09-04 22:53:26.411466288 +0200
+++ /work/SRC/openSUSE:Factory/.python-psygnal.new.25432/python-psygnal.changes 
2023-12-15 21:48:36.661078311 +0100
@@ -1,0 +2,11 @@
+Thu Dec 14 21:12:59 UTC 2023 - Dirk Müller <dmuel...@suse.com>
+
+- update to 0.9.5:
+  * feat: better repr for WeakCallback objects
+  * refactor: make EmitLoop error message clearer
+  * perf: don't compare before/after values in evented
+    dataclass/model when no signals connected
+  * fix: emission of events from root validators and extraneous
+    emission of dependent fields
+
+-------------------------------------------------------------------

Old:
----
  psygnal-0.9.3.tar.gz

New:
----
  psygnal-0.9.5.tar.gz

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

Other differences:
------------------
++++++ python-psygnal.spec ++++++
--- /var/tmp/diff_new_pack.YW8cJ8/_old  2023-12-15 21:48:37.433106717 +0100
+++ /var/tmp/diff_new_pack.YW8cJ8/_new  2023-12-15 21:48:37.433106717 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           python-psygnal
-Version:        0.9.3
+Version:        0.9.5
 Release:        0
 Summary:        Fast python callback/event system modeled after Qt Signals
 License:        BSD-3-Clause

++++++ psygnal-0.9.3.tar.gz -> psygnal-0.9.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/CHANGELOG.md 
new/psygnal-0.9.5/CHANGELOG.md
--- old/psygnal-0.9.3/CHANGELOG.md      2020-02-02 01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/CHANGELOG.md      2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,38 @@
 # Changelog
 
-## [v0.9.3](https://github.com/pyapp-kit/psygnal/tree/v0.9.3) (2023-08-14)
+## [v0.9.5](https://github.com/pyapp-kit/psygnal/tree/v0.9.5) (2023-11-13)
+
+[Full Changelog](https://github.com/pyapp-kit/psygnal/compare/v0.9.4...v0.9.5)
+
+**Implemented enhancements:**
+
+- feat: better repr for WeakCallback objects 
[\#236](https://github.com/pyapp-kit/psygnal/pull/236) 
([tlambert03](https://github.com/tlambert03))
+
+**Merged pull requests:**
+
+- fix: fix py37 build [\#243](https://github.com/pyapp-kit/psygnal/pull/243) 
([tlambert03](https://github.com/tlambert03))
+- ci\(dependabot\): bump pypa/cibuildwheel from 2.16.1 to 2.16.2 
[\#240](https://github.com/pyapp-kit/psygnal/pull/240) 
([dependabot[bot]](https://github.com/apps/dependabot))
+- ci\(dependabot\): bump pypa/cibuildwheel from 2.15.0 to 2.16.1 
[\#238](https://github.com/pyapp-kit/psygnal/pull/238) 
([dependabot[bot]](https://github.com/apps/dependabot))
+- refactor: make EmitLoop error message clearer 
[\#232](https://github.com/pyapp-kit/psygnal/pull/232) 
([tlambert03](https://github.com/tlambert03))
+
+## [v0.9.4](https://github.com/pyapp-kit/psygnal/tree/v0.9.4) (2023-09-19)
+
+[Full Changelog](https://github.com/pyapp-kit/psygnal/compare/v0.9.3...v0.9.4)
+
+**Implemented enhancements:**
+
+- perf: don't compare before/after values in evented dataclass/model when no 
signals connected [\#235](https://github.com/pyapp-kit/psygnal/pull/235) 
([tlambert03](https://github.com/tlambert03))
+
+**Fixed bugs:**
+
+- fix: emission of events from root validators and extraneous emission of 
dependent fields [\#234](https://github.com/pyapp-kit/psygnal/pull/234) 
([tlambert03](https://github.com/tlambert03))
+
+**Merged pull requests:**
+
+- ci\(dependabot\): bump actions/checkout from 3 to 4 
[\#231](https://github.com/pyapp-kit/psygnal/pull/231) 
([dependabot[bot]](https://github.com/apps/dependabot))
+- test: python 3.12 [\#225](https://github.com/pyapp-kit/psygnal/pull/225) 
([tlambert03](https://github.com/tlambert03))
+
+## [v0.9.3](https://github.com/pyapp-kit/psygnal/tree/v0.9.3) (2023-08-15)
 
 [Full Changelog](https://github.com/pyapp-kit/psygnal/compare/v0.9.2...v0.9.3)
 
@@ -10,6 +42,7 @@
 
 **Merged pull requests:**
 
+- build: restrict py versions on cibuildwheel 
[\#229](https://github.com/pyapp-kit/psygnal/pull/229) 
([tlambert03](https://github.com/tlambert03))
 - ci\(dependabot\): bump pypa/cibuildwheel from 2.14.1 to 2.15.0 
[\#227](https://github.com/pyapp-kit/psygnal/pull/227) 
([dependabot[bot]](https://github.com/apps/dependabot))
 
 ## [v0.9.2](https://github.com/pyapp-kit/psygnal/tree/v0.9.2) (2023-08-12)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/LICENSE new/psygnal-0.9.5/LICENSE
--- old/psygnal-0.9.3/LICENSE   2020-02-02 01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/LICENSE   2020-02-02 01:00:00.000000000 +0100
@@ -1,8 +1,4 @@
-
-BSD License
-
 Copyright (c) 2021, Talley Lambert
-All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/PKG-INFO new/psygnal-0.9.5/PKG-INFO
--- old/psygnal-0.9.3/PKG-INFO  2020-02-02 01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/PKG-INFO  2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: psygnal
-Version: 0.9.3
+Version: 0.9.5
 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
@@ -17,6 +17,7 @@
 Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
 Classifier: Typing :: Typed
 Requires-Python: >=3.7
 Requires-Dist: importlib-metadata; python_version < '3.8'
@@ -73,7 +74,7 @@
 
 
[![License](https://img.shields.io/pypi/l/psygnal.svg?color=green)](https://github.com/pyapp-kit/psygnal/raw/master/LICENSE)
 
[![PyPI](https://img.shields.io/pypi/v/psygnal.svg?color=green)](https://pypi.org/project/psygnal)
-![Conda](https://img.shields.io/conda/v/conda-forge/psygnal)
+[![Conda](https://img.shields.io/conda/v/conda-forge/psygnal)](https://github.com/conda-forge/psygnal-feedstock)
 [![Python 
Version](https://img.shields.io/pypi/pyversions/psygnal.svg?color=green)](https://python.org)
 
[![CI](https://github.com/pyapp-kit/psygnal/actions/workflows/test.yml/badge.svg)](https://github.com/pyapp-kit/psygnal/actions/workflows/test.yml)
 
[![codecov](https://codecov.io/gh/pyapp-kit/psygnal/branch/main/graph/badge.svg?token=qGnz9GXpEb)](https://codecov.io/gh/pyapp-kit/psygnal)
@@ -112,7 +113,7 @@
 from psygnal import Signal
 
 class MyObject:
-    # define one or signals as class attributes
+    # define one or more signals as class attributes
     value_changed = Signal(str)
 
 # create an instance
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/README.md new/psygnal-0.9.5/README.md
--- old/psygnal-0.9.3/README.md 2020-02-02 01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/README.md 2020-02-02 01:00:00.000000000 +0100
@@ -2,7 +2,7 @@
 
 
[![License](https://img.shields.io/pypi/l/psygnal.svg?color=green)](https://github.com/pyapp-kit/psygnal/raw/master/LICENSE)
 
[![PyPI](https://img.shields.io/pypi/v/psygnal.svg?color=green)](https://pypi.org/project/psygnal)
-![Conda](https://img.shields.io/conda/v/conda-forge/psygnal)
+[![Conda](https://img.shields.io/conda/v/conda-forge/psygnal)](https://github.com/conda-forge/psygnal-feedstock)
 [![Python 
Version](https://img.shields.io/pypi/pyversions/psygnal.svg?color=green)](https://python.org)
 
[![CI](https://github.com/pyapp-kit/psygnal/actions/workflows/test.yml/badge.svg)](https://github.com/pyapp-kit/psygnal/actions/workflows/test.yml)
 
[![codecov](https://codecov.io/gh/pyapp-kit/psygnal/branch/main/graph/badge.svg?token=qGnz9GXpEb)](https://codecov.io/gh/pyapp-kit/psygnal)
@@ -41,7 +41,7 @@
 from psygnal import Signal
 
 class MyObject:
-    # define one or signals as class attributes
+    # define one or more signals as class attributes
     value_changed = Signal(str)
 
 # create an instance
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/pyproject.toml 
new/psygnal-0.9.5/pyproject.toml
--- old/psygnal-0.9.3/pyproject.toml    2020-02-02 01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/pyproject.toml    2020-02-02 01:00:00.000000000 +0100
@@ -21,6 +21,7 @@
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
     "Typing :: Typed",
 ]
 dynamic = ["version"]
@@ -121,6 +122,7 @@
 [tool.cibuildwheel]
 # Skip 32-bit builds & PyPy wheels on all platforms
 skip = ["*-manylinux_i686", "*-musllinux_i686", "*-win32", "pp*"]
+build = ["cp37-*", "cp38-*", "cp39-*", "cp310-*", "cp311-*"]
 test-extras = ["test"]
 test-command = "pytest {project}/tests -v"
 test-skip = "*-musllinux*"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/src/psygnal/_dataclass_utils.py 
new/psygnal-0.9.5/src/psygnal/_dataclass_utils.py
--- old/psygnal-0.9.3/src/psygnal/_dataclass_utils.py   2020-02-02 
01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/src/psygnal/_dataclass_utils.py   2020-02-02 
01:00:00.000000000 +0100
@@ -182,7 +182,7 @@
                     yield field_name, p_field.annotation
         else:
             for p_field in cls.__fields__.values():  # type: ignore 
[attr-defined]
-                if p_field.field_info.allow_mutation or not exclude_frozen:  # 
type: ignore  # noqa
+                if p_field.field_info.allow_mutation or not exclude_frozen:  # 
type: ignore
                     yield p_field.name, p_field.outer_type_  # type: ignore
         return
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/src/psygnal/_evented_model_v1.py 
new/psygnal-0.9.5/src/psygnal/_evented_model_v1.py
--- old/psygnal-0.9.3/src/psygnal/_evented_model_v1.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/src/psygnal/_evented_model_v1.py  2020-02-02 
01:00:00.000000000 +0100
@@ -40,7 +40,7 @@
 
 _NULL = object()
 ALLOW_PROPERTY_SETTERS = "allow_property_setters"
-PROPERTY_DEPENDENCIES = "property_dependencies"
+FIELD_DEPENDENCIES = "field_dependencies"
 GUESS_PROPERTY_DEPENDENCIES = "guess_property_dependencies"
 
 
@@ -185,16 +185,26 @@
     """
     deps: Dict[str, Set[str]] = {}
 
-    cfg_deps = getattr(cls.__config__, PROPERTY_DEPENDENCIES, {})  # sourcery 
skip
+    cfg_deps = getattr(cls.__config__, FIELD_DEPENDENCIES, {})  # sourcery skip
+    if not cfg_deps:
+        cfg_deps = getattr(cls.__config__, "property_dependencies", {})
+        if cfg_deps:
+            warnings.warn(
+                "The 'property_dependencies' configuration key is deprecated. "
+                "Use 'field_dependencies' instead",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+
     if cfg_deps:
         if not isinstance(cfg_deps, dict):  # pragma: no cover
             raise TypeError(
                 f"Config property_dependencies must be a dict, not 
{cfg_deps!r}"
             )
         for prop, fields in cfg_deps.items():
-            if prop not in cls.__property_setters__:
+            if prop not in {*cls.__fields__, *cls.__property_setters__}:
                 raise ValueError(
-                    "Fields with dependencies must be property.setters."
+                    "Fields with dependencies must be fields or 
property.setters."
                     f"{prop!r} is not."
                 )
             for field in fields:
@@ -342,23 +352,45 @@
             # fallback to default behavior
             return self._super_setattr_(name, value)
 
-        # grab current value
+        # if there are no listeners, we can just set the value without emitting
+        # so first check if there are any listeners for this field or any of 
its
+        # dependent properties.
+        # note that ALL signals will have at least one listener simply by 
nature of
+        # being in the `self._events` SignalGroup.
+        signal_instance: SignalInstance = getattr(self._events, name)
+        deps_with_callbacks = {
+            dep_name
+            for dep_name in self.__field_dependents__.get(name, ())
+            if len(getattr(self._events, dep_name)) > 1
+        }
+        if (
+            len(signal_instance) < 2  # the signal itself has no listeners
+            and not deps_with_callbacks  # no dependent properties with 
listeners
+            and not len(self._events)  # no listeners on the SignalGroup
+        ):
+            return self._super_setattr_(name, value)
+
+        # grab the current value and those of any dependent properties
+        # so that we can check if they have changed after setting the value
         before = getattr(self, name, object())
+        deps_before: Dict[str, Any] = {
+            dep: getattr(self, dep) for dep in deps_with_callbacks
+        }
 
         # set value using original setter
-        signal_instance: SignalInstance = getattr(self._events, name)
         with signal_instance.blocked():
             self._super_setattr_(name, value)
 
-        # if different we emit the event with new value
+        # if the value has changed we emit the event with new value
         after = getattr(self, name)
-
         if not _check_field_equality(type(self), name, after, before):
             signal_instance.emit(after)  # emit event
 
-            # emit events for any dependent computed property setters as well
-            for dep in self.__field_dependents__.get(name, ()):
-                getattr(self.events, dep).emit(getattr(self, dep))
+            # also emit events for any dependent attributes that have changed 
as well
+            for dep, before_val in deps_before.items():
+                after_val = getattr(self, dep)
+                if not _check_field_equality(type(self), dep, after_val, 
before_val):
+                    getattr(self._events, dep).emit(after_val)
 
     # expose the private SignalGroup publically
     @property
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/src/psygnal/_evented_model_v2.py 
new/psygnal-0.9.5/src/psygnal/_evented_model_v2.py
--- old/psygnal-0.9.3/src/psygnal/_evented_model_v2.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/src/psygnal/_evented_model_v2.py  2020-02-02 
01:00:00.000000000 +0100
@@ -41,7 +41,7 @@
 
 _NULL = object()
 ALLOW_PROPERTY_SETTERS = "allow_property_setters"
-PROPERTY_DEPENDENCIES = "property_dependencies"
+FIELD_DEPENDENCIES = "field_dependencies"
 GUESS_PROPERTY_DEPENDENCIES = "guess_property_dependencies"
 
 
@@ -182,16 +182,26 @@
     """
     deps: Dict[str, Set[str]] = {}
 
-    cfg_deps = cls.model_config.get(PROPERTY_DEPENDENCIES, {})  # sourcery skip
+    cfg_deps = cls.model_config.get(FIELD_DEPENDENCIES, {})  # sourcery skip
+    if not cfg_deps:
+        cfg_deps = cls.model_config.get("property_dependencies", {})
+        if cfg_deps:
+            warnings.warn(
+                "The 'property_dependencies' configuration key is deprecated. "
+                "Use 'field_dependencies' instead",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+
     if cfg_deps:
         if not isinstance(cfg_deps, dict):  # pragma: no cover
             raise TypeError(
                 f"Config property_dependencies must be a dict, not 
{cfg_deps!r}"
             )
         for prop, fields in cfg_deps.items():
-            if prop not in cls.__property_setters__:
+            if prop not in {*cls.model_fields, *cls.__property_setters__}:
                 raise ValueError(
-                    "Fields with dependencies must be property.setters."
+                    "Fields with dependencies must be fields or 
property.setters."
                     f"{prop!r} is not."
                 )
             for field in fields:
@@ -328,23 +338,45 @@
             # fallback to default behavior
             return self._super_setattr_(name, value)
 
-        # grab current value
+        # if there are no listeners, we can just set the value without emitting
+        # so first check if there are any listeners for this field or any of 
its
+        # dependent properties.
+        # note that ALL signals will have sat least one listener simply by 
nature of
+        # being in the `self._events` SignalGroup.
+        signal_instance: SignalInstance = getattr(self._events, name)
+        deps_with_callbacks = {
+            dep_name
+            for dep_name in self.__field_dependents__.get(name, ())
+            if len(getattr(self._events, dep_name)) > 1
+        }
+        if (
+            len(signal_instance) < 2  # the signal itself has no listeners
+            and not deps_with_callbacks  # no dependent properties with 
listeners
+            and not len(self._events)  # no listeners on the SignalGroup
+        ):
+            return self._super_setattr_(name, value)
+
+        # grab the current value and those of any dependent properties
+        # so that we can check if they have changed after setting the value
         before = getattr(self, name, object())
+        deps_before: Dict[str, Any] = {
+            dep: getattr(self, dep) for dep in deps_with_callbacks
+        }
 
         # set value using original setter
-        signal_instance: SignalInstance = getattr(self._events, name)
         with signal_instance.blocked():
             self._super_setattr_(name, value)
 
-        # if different we emit the event with new value
+        # if the value has changed we emit the event with new value
         after = getattr(self, name)
-
         if not _check_field_equality(type(self), name, after, before):
             signal_instance.emit(after)  # emit event
 
-            # emit events for any dependent computed property setters as well
-            for dep in self.__field_dependents__.get(name, ()):
-                getattr(self.events, dep).emit(getattr(self, dep))
+            # also emit events for any dependent attributes that have changed 
as well
+            for dep, before_val in deps_before.items():
+                after_val = getattr(self, dep)
+                if not _check_field_equality(type(self), dep, after_val, 
before_val):
+                    getattr(self._events, dep).emit(after_val)
 
     # expose the private SignalGroup publically
     @property
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/src/psygnal/_exceptions.py 
new/psygnal-0.9.5/src/psygnal/_exceptions.py
--- old/psygnal-0.9.3/src/psygnal/_exceptions.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/src/psygnal/_exceptions.py        2020-02-02 
01:00:00.000000000 +0100
@@ -1,11 +1,47 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Callable
+
+from ._weak_callback import WeakCallback
+
+if TYPE_CHECKING:
+    from ._signal import SignalInstance
+
+MSG = """
+While emitting signal {sig!r}, an error occurred in callback {cb!r}.
+The args passed to the callback were: {args!r}
+This is not a bug in psygnal.  See {err!r} above for details.
+"""
+
+
 class EmitLoopError(Exception):
     """Error type raised when an exception occurs during a callback."""
 
-    def __init__(self, slot_repr: str, args: tuple, exc: BaseException) -> 
None:
-        self.slot_repr = slot_repr
+    def __init__(
+        self,
+        cb: WeakCallback | Callable,
+        args: tuple,
+        exc: BaseException,
+        signal: SignalInstance | None = None,
+    ) -> None:
+        self.exc = exc
         self.args = args
         self.__cause__ = exc  # mypyc doesn't set this, but uncompiled code 
would
+        if signal is None:
+            sig_name = ""
+        else:
+            inst_class = signal.instance.__class__
+            mod = getattr(inst_class, "__module__", "")
+            sig_name = f"{mod}.{inst_class.__qualname__}.{signal.name}"
+        if isinstance(cb, WeakCallback):
+            cb_name = cb.slot_repr()
+        else:
+            cb_name = getattr(cb, "__qualname__", repr(cb))
         super().__init__(
-            f"calling {self.slot_repr} with args={args!r} caused "
-            f"{type(exc).__name__}: {exc}."
+            MSG.format(
+                sig=sig_name,
+                cb=cb_name,
+                args=args,
+                err=exc.__class__.__name__,
+            )
         )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/src/psygnal/_group.py 
new/psygnal-0.9.5/src/psygnal/_group.py
--- old/psygnal-0.9.3/src/psygnal/_group.py     2020-02-02 01:00:00.000000000 
+0100
+++ new/psygnal-0.9.5/src/psygnal/_group.py     2020-02-02 01:00:00.000000000 
+0100
@@ -243,7 +243,7 @@
 
     def __repr__(self) -> str:
         """Return repr(self)."""
-        name = f" {self.name!r}" if self.name else ""
+        name = f" {self._name!r}" if self._name else ""
         instance = f" on {self.instance!r}" if self.instance else ""
         nsignals = len(self.signals)
         signals = f"{nsignals} signals" if nsignals > 1 else ""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/src/psygnal/_group_descriptor.py 
new/psygnal-0.9.5/src/psygnal/_group_descriptor.py
--- old/psygnal-0.9.3/src/psygnal/_group_descriptor.py  2020-02-02 
01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/src/psygnal/_group_descriptor.py  2020-02-02 
01:00:00.000000000 +0100
@@ -247,9 +247,10 @@
             if name == signal_group_name:
                 return super_setattr(self, name, value)
 
-            group = getattr(self, signal_group_name, None)
-            signal = cast("SignalInstance | None", getattr(group, name, None))
-            if signal is None:
+            group: SignalGroup | None = getattr(self, signal_group_name, None)
+            signal: SignalInstance | None = getattr(group, name, None)
+            # don't emit if the signal doesn't exist or has no listeners
+            if group is None or signal is None or len(signal) < 2 and not 
len(group):
                 return super_setattr(self, name, value)
 
             with _changes_emitted(self, name, signal):
@@ -422,8 +423,10 @@
 
             # clean up the cache when the instance is deleted
             with contextlib.suppress(TypeError):
-                # mypy says too many attributes for weakref.finalize, but it's 
wrong.
-                weakref.finalize(instance, self._instance_map.pop, obj_id, 
None)  # type: ignore [call-arg]  # noqa
+                # on 3.7 this is type error, above it's not... but mypy yells 
about
+                # type ignore on 3.8+, so we do this funny business instead.
+                args = (instance, self._instance_map.pop, obj_id, None)
+                weakref.finalize(*args)  # type: ignore
 
         return self._instance_map[obj_id]
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/src/psygnal/_queue.py 
new/psygnal-0.9.5/src/psygnal/_queue.py
--- old/psygnal-0.9.3/src/psygnal/_queue.py     2020-02-02 01:00:00.000000000 
+0100
+++ new/psygnal-0.9.5/src/psygnal/_queue.py     2020-02-02 01:00:00.000000000 
+0100
@@ -95,4 +95,4 @@
         try:
             cb(args)
         except Exception as e:  # pragma: no cover
-            raise EmitLoopError(slot_repr=repr(cb), args=args, exc=e) from e
+            raise EmitLoopError(cb=cb, args=args, exc=e) from e
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/src/psygnal/_signal.py 
new/psygnal-0.9.5/src/psygnal/_signal.py
--- old/psygnal-0.9.3/src/psygnal/_signal.py    2020-02-02 01:00:00.000000000 
+0100
+++ new/psygnal-0.9.5/src/psygnal/_signal.py    2020-02-02 01:00:00.000000000 
+0100
@@ -30,10 +30,10 @@
 from ._exceptions import EmitLoopError
 from ._queue import QueuedCallback
 from ._weak_callback import (
+    StrongFunction,
     WeakCallback,
-    _StrongFunction,
-    _WeakSetattr,
-    _WeakSetitem,
+    WeakSetattr,
+    WeakSetitem,
     weak_callback,
 )
 
@@ -343,7 +343,7 @@
 
     def __repr__(self) -> str:
         """Return repr."""
-        name = f" {self.name!r}" if self.name else ""
+        name = f" {self._name!r}" if self._name else ""
         instance = f" on {self.instance!r}" if self.instance is not None else 
""
         return f"<{type(self).__name__}{name}{instance}>"
 
@@ -597,7 +597,7 @@
             raise AttributeError(f"Object {obj} has no attribute {attr!r}")
 
         with self._lock:
-            caller = _WeakSetattr(
+            caller = WeakSetattr(
                 obj,
                 attr,
                 max_args=maxargs,
@@ -630,7 +630,7 @@
         """
         # sourcery skip: merge-nested-ifs, use-next
         with self._lock:
-            cb = _WeakSetattr(obj, attr, on_ref_error="ignore")
+            cb = WeakSetattr(obj, attr, on_ref_error="ignore")
             self._try_discard(cb, missing_ok)
 
     def connect_setitem(
@@ -701,7 +701,7 @@
             raise TypeError(f"Object {obj} does not support __setitem__")
 
         with self._lock:
-            caller = _WeakSetitem(
+            caller = WeakSetitem(
                 obj,  # type: ignore
                 key,
                 max_args=maxargs,
@@ -738,7 +738,7 @@
 
         # sourcery skip: merge-nested-ifs, use-next
         with self._lock:
-            caller = _WeakSetitem(obj, key, on_ref_error="ignore")
+            caller = WeakSetitem(obj, key, on_ref_error="ignore")
             self._try_discard(caller, missing_ok)
 
     def _check_nargs(
@@ -990,7 +990,7 @@
                         caller.cb(args)
                     except Exception as e:
                         raise EmitLoopError(
-                            slot_repr=repr(caller), args=args, exc=e
+                            cb=caller, args=args, exc=e, signal=self
                         ) from e
 
         return None
@@ -1124,7 +1124,7 @@
         )
         dd = {slot: getattr(self, slot) for slot in attrs}
         dd["_instance"] = self._instance()
-        dd["_slots"] = [x for x in self._slots if isinstance(x, 
_StrongFunction)]
+        dd["_slots"] = [x for x in self._slots if isinstance(x, 
StrongFunction)]
         if len(self._slots) > len(dd["_slots"]):
             warnings.warn(
                 "Pickling a SignalInstance does not copy connected weakly 
referenced "
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/src/psygnal/_weak_callback.py 
new/psygnal-0.9.5/src/psygnal/_weak_callback.py
--- old/psygnal-0.9.3/src/psygnal/_weak_callback.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/src/psygnal/_weak_callback.py     2020-02-02 
01:00:00.000000000 +0100
@@ -112,9 +112,9 @@
 
     if isinstance(cb, FunctionType):
         return (
-            _StrongFunction(cb, max_args, args, kwargs)
+            StrongFunction(cb, max_args, args, kwargs)
             if strong_func
-            else _WeakFunction(cb, max_args, args, kwargs, finalize, 
on_ref_error)
+            else WeakFunction(cb, max_args, args, kwargs, finalize, 
on_ref_error)
         )
 
     if isinstance(cb, MethodType):
@@ -126,8 +126,8 @@
                     "WeakCallback.__setitem__ requires a key argument"
                 ) from e
             obj = cast("SupportsSetitem", cb.__self__)
-            return _WeakSetitem(obj, key, max_args, finalize, on_ref_error)
-        return _WeakMethod(cb, max_args, args, kwargs, finalize, on_ref_error)
+            return WeakSetitem(obj, key, max_args, finalize, on_ref_error)
+        return WeakMethod(cb, max_args, args, kwargs, finalize, on_ref_error)
 
     if isinstance(cb, (MethodWrapperType, BuiltinMethodType)):
         if kwargs:  # pragma: no cover
@@ -142,8 +142,8 @@
                 raise TypeError(
                     "setattr requires two arguments, an object and an 
attribute name."
                 ) from e
-            return _WeakSetattr(obj, attr, max_args, finalize, on_ref_error)
-        return _WeakBuiltin(cb, max_args, args, finalize, on_ref_error)
+            return WeakSetattr(obj, attr, max_args, finalize, on_ref_error)
+        return WeakBuiltin(cb, max_args, args, finalize, on_ref_error)
 
     if _is_toolz_curry(cb):
         cb_partial = getattr(cb, "_partial", None)
@@ -161,7 +161,7 @@
         )
 
     if callable(cb):
-        return _WeakFunction(cb, max_args, args, kwargs, finalize, 
on_ref_error)
+        return WeakFunction(cb, max_args, args, kwargs, finalize, on_ref_error)
 
     raise TypeError(f"unsupported type {type(cb)}")  # pragma: no cover
 
@@ -188,6 +188,9 @@
         on_ref_error: RefErrorChoice = "warn",
     ) -> None:
         self._key: str = WeakCallback.object_key(obj)
+        self._obj_module: str = getattr(obj, "__module__", None) or ""
+        self._obj_qualname: str = getattr(obj, "__qualname__", "")
+        self._object_repr: str = WeakCallback.object_repr(obj)
         self._max_args: int | None = max_args
         self._alive: bool = True
         self._on_ref_error: RefErrorChoice = on_ref_error
@@ -236,6 +239,9 @@
 
             return _strong_ref
 
+    def slot_repr(self) -> str:
+        return f"{self._obj_module}.{self._obj_qualname}"
+
     @staticmethod
     def object_key(obj: Any) -> str:
         """Return a unique key for an object.
@@ -258,6 +264,28 @@
             obj_name = getattr(obj, "__name__", None) or ""
         return f"{module}:{obj_name}@{hex(obj_id)}"
 
+    @staticmethod
+    def object_repr(obj: Any) -> str:
+        """Return a human-readable repr for obj."""
+        module = getattr(obj, "__module__", "")
+        if hasattr(obj, "__self__"):
+            # bound method ... don't take the id of the bound method itself.
+            owner_cls = type(obj.__self__)
+            module = getattr(owner_cls, "__module__", None) or ""
+            method_name = getattr(obj, "__name__", None) or ""
+            if module == "builtins":
+                return method_name
+            type_qname = getattr(owner_cls, "__qualname__", "")
+            return f"{module}.{type_qname}.{method_name}"
+        elif getattr(obj, "__qualname__", ""):
+            return f"{module}.{obj.__qualname__}"
+        elif getattr(type(obj), "__qualname__", ""):
+            return f"{module}.{type(obj).__qualname__}"
+        return repr(obj)
+
+    def __repr__(self) -> str:
+        return f"<{self.__class__.__name__} on {self._object_repr}>"
+
 
 def _kill_and_finalize(
     wcb: WeakCallback, finalize: Callable[[WeakCallback], Any]
@@ -271,7 +299,7 @@
 
 
 @mypyc_attr(serializable=True)
-class _StrongFunction(WeakCallback):
+class StrongFunction(WeakCallback):
     """Wrapper around a strong function reference."""
 
     def __init__(
@@ -287,6 +315,9 @@
         self._args = args
         self._kwargs = kwargs or {}
 
+        if args:
+            self._object_repr = 
f"{self._object_repr}{(*args,)!r}".replace(")", " ...)")
+
     def cb(self, args: tuple[Any, ...] = ()) -> None:
         if self._max_args is not None:
             args = args[: self._max_args]
@@ -306,7 +337,7 @@
             setattr(self, k, v)
 
 
-class _WeakFunction(WeakCallback):
+class WeakFunction(WeakCallback):
     """Wrapper around a weak function reference."""
 
     def __init__(
@@ -323,6 +354,9 @@
         self._args = args
         self._kwargs = kwargs or {}
 
+        if args:
+            self._object_repr = 
f"{self._object_repr}{(*args,)!r}".replace(")", " ...)")
+
     def cb(self, args: tuple[Any, ...] = ()) -> None:
         f = self._f()
         if f is None:
@@ -340,7 +374,7 @@
         return f
 
 
-class _WeakMethod(WeakCallback):
+class WeakMethod(WeakCallback):
     """Wrapper around a method bound to a weakly-referenced object.
 
     Bound methods have a `__self__` attribute that holds a strong reference to 
the
@@ -360,11 +394,18 @@
         finalize: Callable | None = None,
         on_ref_error: RefErrorChoice = "warn",
     ) -> None:
-        super().__init__(obj.__self__, max_args, on_ref_error)
+        super().__init__(obj, max_args, on_ref_error)
         self._obj_ref = self._try_ref(obj.__self__, finalize)
         self._func_ref = self._try_ref(obj.__func__, finalize)
         self._args = args
         self._kwargs = kwargs or {}
+        if args:
+            self._object_repr = 
f"{self._object_repr}{(*args,)!r}".replace(")", " ...)")
+
+    def slot_repr(self) -> str:
+        obj = self._obj_ref()
+        func_name = getattr(self._func_ref(), "__name__", "<method>")
+        return f"{self._obj_module}.{obj.__class__.__qualname__}.{func_name}"
 
     def cb(self, args: tuple[Any, ...] = ()) -> None:
         obj = self._obj_ref()
@@ -387,7 +428,7 @@
         return method
 
 
-class _WeakBuiltin(WeakCallback):
+class WeakBuiltin(WeakCallback):
     """Wrapper around a c-based method on a weakly-referenced object.
 
     Builtin/extension methods do have a `__self__` attribute (the object to 
which they
@@ -410,6 +451,12 @@
         self._obj_ref = self._try_ref(obj.__self__, finalize)
         self._func_name = obj.__name__
         self._args = args
+        if args:
+            self._object_repr = 
f"{self._object_repr}{(*args,)!r}".replace(")", " ...)")
+
+    def slot_repr(self) -> str:
+        obj = self._obj_ref()
+        return f"{obj.__class__.__qualname__}.{self._func_name}"
 
     def cb(self, args: tuple[Any, ...] = ()) -> None:
         func = getattr(self._obj_ref(), self._func_name, None)
@@ -424,7 +471,7 @@
         return getattr(self._obj_ref(), self._func_name, None)
 
 
-class _WeakSetattr(WeakCallback):
+class WeakSetattr(WeakCallback):
     """Caller to set an attribute on a weakly-referenced object."""
 
     def __init__(
@@ -439,6 +486,11 @@
         self._key += f".__setattr__({attr!r})"
         self._obj_ref = self._try_ref(obj, finalize)
         self._attr = attr
+        self._object_repr += f".__setattr__({attr!r}, ...)"
+
+    def slot_repr(self) -> str:
+        obj = self._obj_ref()
+        return f"setattr({obj.__class__.__qualname__}, {self._attr!r}, ...)"
 
     def cb(self, args: tuple[Any, ...] = ()) -> None:
         obj = self._obj_ref()
@@ -458,7 +510,7 @@
         ...
 
 
-class _WeakSetitem(WeakCallback):
+class WeakSetitem(WeakCallback):
     """Caller to call __setitem__ on a weakly-referenced object."""
 
     def __init__(
@@ -473,6 +525,11 @@
         self._key += f".__setitem__({key!r})"
         self._obj_ref = self._try_ref(obj, finalize)
         self._itemkey = key
+        self._object_repr += f".__setitem__({key!r}, ...)"
+
+    def slot_repr(self) -> str:
+        obj = self._obj_ref()
+        return f"{obj.__class__.__qualname__}.__setitem__({self._itemkey!r}, 
...)"
 
     def cb(self, args: tuple[Any, ...] = ()) -> None:
         obj = self._obj_ref()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/tests/test_dataclass_utils.py 
new/psygnal-0.9.5/tests/test_dataclass_utils.py
--- old/psygnal-0.9.3/tests/test_dataclass_utils.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/tests/test_dataclass_utils.py     2020-02-02 
01:00:00.000000000 +0100
@@ -7,8 +7,8 @@
 
 try:
     from msgspec import Struct
-except ImportError:
-    Struct = None
+except (ImportError, TypeError):  # type error on python 3.12-dev
+    Struct = None  # type: ignore [assignment,misc]
 
 try:
     from pydantic import __version__ as pydantic_version
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/tests/test_evented_model.py 
new/psygnal-0.9.5/tests/test_evented_model.py
--- old/psygnal-0.9.3/tests/test_evented_model.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/tests/test_evented_model.py       2020-02-02 
01:00:00.000000000 +0100
@@ -1,7 +1,7 @@
 import inspect
 import sys
 from typing import Any, ClassVar, List, Sequence, Union
-from unittest.mock import Mock
+from unittest.mock import Mock, call, patch
 
 import numpy as np
 import pytest
@@ -15,7 +15,7 @@
 import pydantic.version
 from pydantic import BaseModel
 
-from psygnal import EventedModel, SignalGroup
+from psygnal import EmissionInfo, EventedModel, SignalGroup
 
 PYDANTIC_V2 = pydantic.version.VERSION.startswith("2")
 
@@ -188,14 +188,14 @@
         """
 
         id: int
-        name: str = "A"
+        user_name: str = "A"
         age: ClassVar[int] = 100
 
     user1 = User(id=0)
-    user2 = User(id=1, name="K")
+    user2 = User(id=1, user_name="K")
     # Check user1 and user2 dicts
-    assert asdict(user1) == {"id": 0, "name": "A"}
-    assert asdict(user2) == {"id": 1, "name": "K"}
+    assert asdict(user1) == {"id": 0, "user_name": "A"}
+    assert asdict(user2) == {"id": 1, "user_name": "K"}
 
     # Add mocks
     user1_events = Mock()
@@ -207,18 +207,27 @@
 
     # Update user1 from user2
     user1.update(user2)
-    assert asdict(user1) == {"id": 1, "name": "K"}
+    assert asdict(user1) == {"id": 1, "user_name": "K"}
 
     u1_id_events.assert_called_with(1)
     u2_id_events.assert_not_called()
-    assert user1_events.call_count == 2
+
+    # NOTE:
+    # user.events.user_name is NOT actually emitted because it has no callbacks
+    # connected to it.  see test_comparison_count below...
+    user1_events.assert_has_calls(
+        [
+            call(EmissionInfo(signal=user1.events.id, args=(1,))),
+            # call(EmissionInfo(signal=user1.events.user_name, args=("K",))),
+        ]
+    )
     u1_id_events.reset_mock()
     u2_id_events.reset_mock()
     user1_events.reset_mock()
 
     # Update user1 from user2 again, no event emission expected
     user1.update(user2)
-    assert asdict(user1) == {"id": 1, "name": "K"}
+    assert asdict(user1) == {"id": 1, "user_name": "K"}
 
     u1_id_events.assert_not_called()
     u2_id_events.assert_not_called()
@@ -481,13 +490,13 @@
         if PYDANTIC_V2:
             model_config = {
                 "allow_property_setters": True,
-                "property_dependencies": {"c": ["a", "b"]},
+                "field_dependencies": {"c": ["a", "b"]},
             }
         else:
 
             class Config:
                 allow_property_setters = True
-                property_dependencies = {"c": ["a", "b"]}
+                field_dependencies = {"c": ["a", "b"]}
 
     assert list(MyModel.__property_setters__) == ["c"]
     # the metaclass should have figured out that both a and b affect c
@@ -542,8 +551,10 @@
     assert t.c == [5, 20]
 
 
-def test_non_setter_with_dependencies():
-    with pytest.raises(ValueError) as e:
+def test_non_setter_with_dependencies() -> None:
+    with pytest.raises(
+        ValueError, match="Fields with dependencies must be fields or 
property.setters"
+    ):
 
         class M(EventedModel):
             x: int
@@ -559,19 +570,17 @@
             if PYDANTIC_V2:
                 model_config = {
                     "allow_property_setters": True,
-                    "property_dependencies": {"a": []},
+                    "field_dependencies": {"a": []},
                 }
             else:
 
                 class Config:
                     allow_property_setters = True
-                    property_dependencies = {"a": []}
-
-    assert "Fields with dependencies must be property.setters" in str(e.value)
+                    field_dependencies = {"a": []}
 
 
 def test_unrecognized_property_dependencies():
-    with pytest.warns(UserWarning) as e:
+    with pytest.warns(UserWarning, match="Unrecognized field dependency: 'b'"):
 
         class M(EventedModel):
             x: int
@@ -587,15 +596,13 @@
             if PYDANTIC_V2:
                 model_config = {
                     "allow_property_setters": True,
-                    "property_dependencies": {"y": ["b"]},
+                    "field_dependencies": {"y": ["b"]},
                 }
             else:
 
                 class Config:
                     allow_property_setters = True
-                    property_dependencies = {"y": ["b"]}
-
-    assert "Unrecognized field dependency: 'b'" in str(e[0])
+                    field_dependencies = {"y": ["b"]}
 
 
 @pytest.mark.skipif(PYDANTIC_V2, reason="pydantic 2 does not support this")
@@ -671,13 +678,13 @@
         if PYDANTIC_V2:
             model_config = {
                 "allow_property_setters": True,
-                "property_dependencies": {"b": ["a"]},
+                "field_dependencies": {"b": ["a"]},
             }
         else:
 
             class Config:
                 allow_property_setters = True
-                property_dependencies = {"b": ["a"]}
+                field_dependencies = {"b": ["a"]}
 
     mock_a = Mock()
     mock_b = Mock()
@@ -687,3 +694,155 @@
     m.b = 3
     mock_a.assert_called_once_with(2)
     mock_b.assert_called_once_with(3)
+
+
+def test_root_validator_events():
+    class Model(EventedModel):
+        x: int
+        y: int
+
+        if PYDANTIC_V2:
+            from pydantic import model_validator
+
+            model_config = {
+                "validate_assignment": True,
+                "field_dependencies": {"y": ["x"]},
+            }
+
+            @model_validator(mode="before")
+            def check(cls, values: dict) -> dict:
+                x = values["x"]
+                values["y"] = min(values["y"], x)
+                return values
+
+        else:
+            from pydantic import root_validator
+
+            class Config:
+                validate_assignment = True
+                field_dependencies = {"y": ["x"]}
+
+            @root_validator
+            def check(cls, values: dict) -> dict:
+                x = values["x"]
+                values["y"] = min(values["y"], x)
+                return values
+
+    m = Model(x=2, y=1)
+    xmock = Mock()
+    ymock = Mock()
+    m.events.x.connect(xmock)
+    m.events.y.connect(ymock)
+    m.x = 0
+    assert m.y == 0
+    xmock.assert_called_once_with(0)
+    ymock.assert_called_once_with(0)
+
+    xmock.reset_mock()
+    ymock.reset_mock()
+
+    m.x = 2
+    assert m.y == 0
+    xmock.assert_called_once_with(2)
+    ymock.assert_not_called()
+
+
+def test_deprecation() -> None:
+    with pytest.warns(DeprecationWarning, match="Use 'field_dependencies' 
instead"):
+
+        class MyModel(EventedModel):
+            a: int = 1
+            b: int = 1
+
+            if PYDANTIC_V2:
+                model_config = {"property_dependencies": {"a": ["b"]}}
+            else:
+
+                class Config:
+                    property_dependencies = {"a": ["b"]}
+
+        assert MyModel.__field_dependents__ == {"b": {"a"}}
+
+
+def test_comparison_count() -> None:
+    """Test that we only compare fields that are actually connected to 
events."""
+
+    class Model(EventedModel):
+        a: int
+
+        @property
+        def b(self) -> int:
+            return self.a + 1
+
+        @b.setter
+        def b(self, b: int) -> None:
+            self.a = b - 1
+
+        if PYDANTIC_V2:
+            model_config = {
+                "allow_property_setters": True,
+                "field_dependencies": {"b": ["a"]},
+            }
+        else:
+
+            class Config:
+                allow_property_setters = True
+                field_dependencies = {"b": ["a"]}
+
+    # pick whether to mock v1 or v2 modules
+    model_module = sys.modules[type(Model).__module__]
+
+    m = Model(a=0)
+    b_mock = Mock()
+    with patch.object(
+        model_module,
+        "_check_field_equality",
+        wraps=model_module._check_field_equality,
+    ) as check_mock:
+        m.a = 1
+
+    check_mock.assert_not_called()
+    b_mock.assert_not_called()
+
+    m.events.b.connect(b_mock)
+    with patch.object(
+        model_module,
+        "_check_field_equality",
+        wraps=model_module._check_field_equality,
+    ) as check_mock:
+        m.a = 3
+    check_mock.assert_has_calls([call(Model, "a", 3, 1), call(Model, "b", 4, 
2)])
+    b_mock.assert_called_once_with(4)
+
+
+def test_connect_only_to_events() -> None:
+    """Make sure that we still make comparison and emit events when connecting
+    only to the events group itself."""
+
+    class Model(EventedModel):
+        a: int
+
+    # pick whether to mock v1 or v2 modules
+    model_module = sys.modules[type(Model).__module__]
+
+    m = Model(a=0)
+    mock1 = Mock()
+    with patch.object(
+        model_module,
+        "_check_field_equality",
+        wraps=model_module._check_field_equality,
+    ) as check_mock:
+        m.a = 1
+
+    check_mock.assert_not_called()
+    mock1.assert_not_called()
+
+    m.events.connect(mock1)
+    with patch.object(
+        model_module,
+        "_check_field_equality",
+        wraps=model_module._check_field_equality,
+    ) as check_mock:
+        m.a = 3
+    check_mock.assert_has_calls([call(Model, "a", 3, 1)])
+    mock1.assert_called_once()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/psygnal-0.9.3/tests/test_weak_callable.py 
new/psygnal-0.9.5/tests/test_weak_callable.py
--- old/psygnal-0.9.3/tests/test_weak_callable.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/psygnal-0.9.5/tests/test_weak_callable.py       2020-02-02 
01:00:00.000000000 +0100
@@ -1,5 +1,6 @@
 import gc
 from functools import partial
+from typing import Any
 from unittest.mock import Mock
 from weakref import ref
 
@@ -178,7 +179,7 @@
     assert dp.keywords == p.keywords
 
 
-def test_queued_callbacks():
+def test_queued_callbacks() -> None:
     from psygnal._queue import QueuedCallback
 
     def func(x):
@@ -189,3 +190,27 @@
 
     assert qcb.dereference() is func
     assert qcb(1) == 1
+
+
+def test_cb_raises() -> None:
+    from psygnal import EmitLoopError
+
+    m = str(EmitLoopError(weak_callback(print), (1,), RuntimeError("test")))
+    assert "an error occurred in callback 'module.print'" in m
+    m = str(EmitLoopError(print, (1,), RuntimeError("test")))
+    assert " an error occurred in callback 'print'" in m
+
+    class T:
+        x = 1
+
+        def __setitem__(self, *_: Any) -> Any:
+            pass
+
+    t = T()
+    cb = weak_callback(setattr, t, "x")
+    m = str(EmitLoopError(cb, (2,), RuntimeError("test")))
+    assert 'an error occurred in callback "setattr' in m
+
+    cb = weak_callback(t.__setitem__, "x")
+    m = str(EmitLoopError(cb, (2,), RuntimeError("test")))
+    assert ".T.__setitem__" in m

Reply via email to