From 588118d6d398803adb25e33c9a11eb4b0269285e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 20 Dec 2022 14:12:43 -0500 Subject: [PATCH 1/8] fix: fix static init typing on eventedmodel --- src/psygnal/_evented_model.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/psygnal/_evented_model.py b/src/psygnal/_evented_model.py index 3f8e853a..4bc5b2a8 100644 --- a/src/psygnal/_evented_model.py +++ b/src/psygnal/_evented_model.py @@ -8,17 +8,15 @@ Any, Callable, ClassVar, - Dict, Iterator, - Set, Type, - Union, cast, no_type_check, ) import pydantic.main from pydantic import BaseModel, PrivateAttr, utils +from pydantic.fields import Field, FieldInfo from ._evented_decorator import _check_field_equality, _pick_equality_operator from ._group import SignalGroup @@ -29,10 +27,19 @@ from pydantic import BaseConfig from pydantic.fields import ModelField + from typing_extensions import dataclass_transform ConfigType = Type[BaseConfig] EqOperator = Callable[[Any, Any], bool] +else: + try: + from typing_extensions import dataclass_transform + except ImportError: + + def dataclass_transform(*args, **kwargs): + return lambda a: a + _NULL = object() ALLOW_PROPERTY_SETTERS = "allow_property_setters" PROPERTY_DEPENDENCIES = "property_dependencies" @@ -84,6 +91,7 @@ def _return2(x: str, y: inspect.Signature) -> inspect.Signature: pydantic.main.ClassAttribute = utils.ClassAttribute # type: ignore +@dataclass_transform(kw_only_default=True, field_descriptors=(Field, FieldInfo)) class EventedMetaclass(pydantic.main.ModelMetaclass): """pydantic ModelMetaclass that preps "equality checking" operations. @@ -108,7 +116,7 @@ def __new__( # noqa: C901 cls.__eq_operators__ = {} signals = {} - fields: Dict[str, ModelField] = cls.__fields__ + fields: dict[str, ModelField] = cls.__fields__ for n, f in fields.items(): cls.__eq_operators__[n] = _pick_equality_operator(f.type_) if f.field_info.allow_mutation: @@ -153,7 +161,7 @@ def __new__( # noqa: C901 return cls -def _get_field_dependents(cls: EventedModel) -> Dict[str, Set[str]]: # noqa: C901 +def _get_field_dependents(cls: EventedModel) -> dict[str, set[str]]: # noqa: C901 """Return mapping of field name -> dependent set of property names. Dependencies may be declared in the Model Config to emit an event @@ -177,7 +185,7 @@ def c(self, val: Sequence[int]): class Config: property_dependencies={'c': ['a', 'b']} """ - deps: Dict[str, Set[str]] = {} + deps: dict[str, set[str]] = {} cfg_deps = getattr(cls.__config__, PROPERTY_DEPENDENCIES, {}) # sourcery skip if cfg_deps: @@ -291,13 +299,13 @@ class Config: _events: SignalGroup = PrivateAttr() # mapping of name -> property obj for methods that are property setters - __property_setters__: ClassVar[Dict[str, property]] + __property_setters__: ClassVar[dict[str, property]] # mapping of field name -> dependent set of property names # when field is changed, an event for dependent properties will be emitted. - __field_dependents__: ClassVar[Dict[str, Set[str]]] - __eq_operators__: ClassVar[Dict[str, EqOperator]] + __field_dependents__: ClassVar[dict[str, set[str]]] + __eq_operators__: ClassVar[dict[str, EqOperator]] __slots__ = {"__weakref__"} - __signal_group__: ClassVar[Type[SignalGroup]] + __signal_group__: ClassVar[type[SignalGroup]] # pydantic BaseModel configuration. see: # https://pydantic-docs.helpmanual.io/usage/model_config/ @@ -365,7 +373,7 @@ def reset(self) -> None: ): setattr(self, name, value) - def update(self, values: Union[EventedModel, dict], recurse: bool = True) -> None: + def update(self, values: EventedModel | dict, recurse: bool = True) -> None: """Update a model in place. Parameters @@ -434,7 +442,7 @@ def enums_as_values(self, as_values: bool = True) -> Iterator[None]: delattr(self.Config, "use_enum_values") -def _get_defaults(obj: BaseModel) -> Dict[str, Any]: +def _get_defaults(obj: BaseModel) -> dict[str, Any]: """Get possibly nested default values for a Model object.""" dflt = {} for k, v in obj.__fields__.items(): From 713f83bbe136728600ffb045e5d66b06ff3f0ff0 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 20 Dec 2022 14:14:26 -0500 Subject: [PATCH 2/8] undo typing changes --- src/psygnal/_evented_model.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/psygnal/_evented_model.py b/src/psygnal/_evented_model.py index 4bc5b2a8..b1cac2c4 100644 --- a/src/psygnal/_evented_model.py +++ b/src/psygnal/_evented_model.py @@ -8,8 +8,11 @@ Any, Callable, ClassVar, + Dict, Iterator, + Set, Type, + Union, cast, no_type_check, ) @@ -116,7 +119,7 @@ def __new__( # noqa: C901 cls.__eq_operators__ = {} signals = {} - fields: dict[str, ModelField] = cls.__fields__ + fields: Dict[str, ModelField] = cls.__fields__ for n, f in fields.items(): cls.__eq_operators__[n] = _pick_equality_operator(f.type_) if f.field_info.allow_mutation: @@ -161,7 +164,7 @@ def __new__( # noqa: C901 return cls -def _get_field_dependents(cls: EventedModel) -> dict[str, set[str]]: # noqa: C901 +def _get_field_dependents(cls: EventedModel) -> Dict[str, Set[str]]: # noqa: C901 """Return mapping of field name -> dependent set of property names. Dependencies may be declared in the Model Config to emit an event @@ -185,7 +188,7 @@ def c(self, val: Sequence[int]): class Config: property_dependencies={'c': ['a', 'b']} """ - deps: dict[str, set[str]] = {} + deps: Dict[str, Set[str]] = {} cfg_deps = getattr(cls.__config__, PROPERTY_DEPENDENCIES, {}) # sourcery skip if cfg_deps: @@ -299,13 +302,13 @@ class Config: _events: SignalGroup = PrivateAttr() # mapping of name -> property obj for methods that are property setters - __property_setters__: ClassVar[dict[str, property]] + __property_setters__: ClassVar[Dict[str, property]] # mapping of field name -> dependent set of property names # when field is changed, an event for dependent properties will be emitted. - __field_dependents__: ClassVar[dict[str, set[str]]] - __eq_operators__: ClassVar[dict[str, EqOperator]] + __field_dependents__: ClassVar[Dict[str, Set[str]]] + __eq_operators__: ClassVar[Dict[str, EqOperator]] __slots__ = {"__weakref__"} - __signal_group__: ClassVar[type[SignalGroup]] + __signal_group__: ClassVar[Type[SignalGroup]] # pydantic BaseModel configuration. see: # https://pydantic-docs.helpmanual.io/usage/model_config/ @@ -373,7 +376,7 @@ def reset(self) -> None: ): setattr(self, name, value) - def update(self, values: EventedModel | dict, recurse: bool = True) -> None: + def update(self, values: Union[EventedModel, dict], recurse: bool = True) -> None: """Update a model in place. Parameters @@ -442,7 +445,7 @@ def enums_as_values(self, as_values: bool = True) -> Iterator[None]: delattr(self.Config, "use_enum_values") -def _get_defaults(obj: BaseModel) -> dict[str, Any]: +def _get_defaults(obj: BaseModel) -> Dict[str, Any]: """Get possibly nested default values for a Model object.""" dflt = {} for k, v in obj.__fields__.items(): From 9fd0acbecfee4a5b9afa4fe567ce93b897d9d733 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 20 Dec 2022 14:22:36 -0500 Subject: [PATCH 3/8] class var --- src/psygnal/_evented_model.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/psygnal/_evented_model.py b/src/psygnal/_evented_model.py index b1cac2c4..ab70282f 100644 --- a/src/psygnal/_evented_model.py +++ b/src/psygnal/_evented_model.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import sys import warnings from contextlib import contextmanager @@ -28,11 +26,9 @@ if TYPE_CHECKING: import inspect - from pydantic import BaseConfig from pydantic.fields import ModelField from typing_extensions import dataclass_transform - ConfigType = Type[BaseConfig] EqOperator = Callable[[Any, Any], bool] else: @@ -43,6 +39,7 @@ def dataclass_transform(*args, **kwargs): return lambda a: a + _NULL = object() ALLOW_PROPERTY_SETTERS = "allow_property_setters" PROPERTY_DEPENDENCIES = "property_dependencies" @@ -94,7 +91,7 @@ def _return2(x: str, y: inspect.Signature) -> inspect.Signature: pydantic.main.ClassAttribute = utils.ClassAttribute # type: ignore -@dataclass_transform(kw_only_default=True, field_descriptors=(Field, FieldInfo)) +@dataclass_transform(kw_only_default=True, field_specifiers=(Field, FieldInfo)) class EventedMetaclass(pydantic.main.ModelMetaclass): """pydantic ModelMetaclass that preps "equality checking" operations. @@ -112,14 +109,14 @@ class EventedMetaclass(pydantic.main.ModelMetaclass): @no_type_check def __new__( # noqa: C901 mcs: type, name: str, bases: tuple, namespace: dict, **kwargs: Any - ) -> EventedMetaclass: + ) -> "EventedMetaclass": """Create new EventedModel class.""" with no_class_attributes(): cls = super().__new__(mcs, name, bases, namespace, **kwargs) cls.__eq_operators__ = {} signals = {} - fields: Dict[str, ModelField] = cls.__fields__ + fields: Dict[str, 'ModelField'] = cls.__fields__ for n, f in fields.items(): cls.__eq_operators__[n] = _pick_equality_operator(f.type_) if f.field_info.allow_mutation: @@ -164,7 +161,7 @@ def __new__( # noqa: C901 return cls -def _get_field_dependents(cls: EventedModel) -> Dict[str, Set[str]]: # noqa: C901 +def _get_field_dependents(cls: "EventedModel") -> Dict[str, Set[str]]: # noqa: C901 """Return mapping of field name -> dependent set of property names. Dependencies may be declared in the Model Config to emit an event @@ -299,14 +296,14 @@ class Config: """ # add private attributes for event emission - _events: SignalGroup = PrivateAttr() + _events: ClassVar[SignalGroup] = PrivateAttr() # mapping of name -> property obj for methods that are property setters __property_setters__: ClassVar[Dict[str, property]] # mapping of field name -> dependent set of property names # when field is changed, an event for dependent properties will be emitted. __field_dependents__: ClassVar[Dict[str, Set[str]]] - __eq_operators__: ClassVar[Dict[str, EqOperator]] + __eq_operators__: ClassVar[Dict[str, "EqOperator"]] __slots__ = {"__weakref__"} __signal_group__: ClassVar[Type[SignalGroup]] # pydantic BaseModel configuration. see: @@ -376,7 +373,7 @@ def reset(self) -> None: ): setattr(self, name, value) - def update(self, values: Union[EventedModel, dict], recurse: bool = True) -> None: + def update(self, values: Union["EventedModel", dict], recurse: bool = True) -> None: """Update a model in place. Parameters From 843b8bafb9f69fe147e7bc6710ca05893ec7c30c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 19:23:35 +0000 Subject: [PATCH 4/8] style(pre-commit.ci): auto fixes [...] --- src/psygnal/_evented_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psygnal/_evented_model.py b/src/psygnal/_evented_model.py index ab70282f..ee47a798 100644 --- a/src/psygnal/_evented_model.py +++ b/src/psygnal/_evented_model.py @@ -116,7 +116,7 @@ def __new__( # noqa: C901 cls.__eq_operators__ = {} signals = {} - fields: Dict[str, 'ModelField'] = cls.__fields__ + fields: Dict[str, "ModelField"] = cls.__fields__ for n, f in fields.items(): cls.__eq_operators__[n] = _pick_equality_operator(f.type_) if f.field_info.allow_mutation: From be6641347153bbdb94311bf34f4b13c93372aca7 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 20 Dec 2022 14:32:02 -0500 Subject: [PATCH 5/8] fix: ignore error --- src/psygnal/_evented_model.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/psygnal/_evented_model.py b/src/psygnal/_evented_model.py index ab70282f..812464eb 100644 --- a/src/psygnal/_evented_model.py +++ b/src/psygnal/_evented_model.py @@ -116,7 +116,7 @@ def __new__( # noqa: C901 cls.__eq_operators__ = {} signals = {} - fields: Dict[str, 'ModelField'] = cls.__fields__ + fields: Dict[str, "ModelField"] = cls.__fields__ for n, f in fields.items(): cls.__eq_operators__[n] = _pick_equality_operator(f.type_) if f.field_info.allow_mutation: @@ -315,7 +315,11 @@ class Config: def __init__(_model_self_, **data: Any) -> None: super().__init__(**data) - _model_self_._events = _model_self_.__signal_group__(_model_self_) + Group = _model_self_.__signal_group__ + # the type error is "cannot assign to a class variable" ... + # but if we don't use `ClassVar`, then the `dataclass_transform` decorator + # will add _events: SignalGroup to the __init__ signature, for *all* user models + _model_self_._events = Group(_model_self_) # type: ignore [misc] def _super_setattr_(self, name: str, value: Any) -> None: # pydantic will raise a ValueError if extra fields are not allowed From b6f7b26b5c38c075dde9214b6e73fb72bb35e9b4 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 20 Dec 2022 15:11:40 -0500 Subject: [PATCH 6/8] fix: fix annotation --- src/psygnal/_evented_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/psygnal/_evented_model.py b/src/psygnal/_evented_model.py index 812464eb..5fe706be 100644 --- a/src/psygnal/_evented_model.py +++ b/src/psygnal/_evented_model.py @@ -24,7 +24,7 @@ from ._signal import Signal, SignalInstance if TYPE_CHECKING: - import inspect + from inspect import Signature from pydantic.fields import ModelField from typing_extensions import dataclass_transform @@ -80,7 +80,7 @@ def no_class_attributes() -> Iterator[None]: # pragma: no cover # monkey patch the pydantic ClassAttribute object # the second argument to ClassAttribute is the inspect.Signature object - def _return2(x: str, y: inspect.Signature) -> inspect.Signature: + def _return2(x: str, y: 'Signature') -> 'Signature': return y pydantic.main.ClassAttribute = _return2 # type: ignore From 97e1d7075230de60f592af46b44d13795d3ed7eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 20:11:55 +0000 Subject: [PATCH 7/8] style(pre-commit.ci): auto fixes [...] --- src/psygnal/_evented_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psygnal/_evented_model.py b/src/psygnal/_evented_model.py index 5fe706be..80911d10 100644 --- a/src/psygnal/_evented_model.py +++ b/src/psygnal/_evented_model.py @@ -80,7 +80,7 @@ def no_class_attributes() -> Iterator[None]: # pragma: no cover # monkey patch the pydantic ClassAttribute object # the second argument to ClassAttribute is the inspect.Signature object - def _return2(x: str, y: 'Signature') -> 'Signature': + def _return2(x: str, y: "Signature") -> "Signature": return y pydantic.main.ClassAttribute = _return2 # type: ignore From aa734afca825bf46957dd67eb364b9f0b4e28cd1 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 20 Dec 2022 15:17:18 -0500 Subject: [PATCH 8/8] fix: fix coverage --- src/psygnal/_evented_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/psygnal/_evented_model.py b/src/psygnal/_evented_model.py index 80911d10..eedc4346 100644 --- a/src/psygnal/_evented_model.py +++ b/src/psygnal/_evented_model.py @@ -34,7 +34,7 @@ else: try: from typing_extensions import dataclass_transform - except ImportError: + except ImportError: # pragma: no cover def dataclass_transform(*args, **kwargs): return lambda a: a