commit python-psygnal for openSUSE:Factory
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 <dmueller@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
participants (1)
-
Source-Sync