Skip to content

Commit

Permalink
fix: fix use of computed_field setter with property_setters
Browse files Browse the repository at this point in the history
  • Loading branch information
tlambert03 committed Nov 8, 2024
1 parent 37bd312 commit 56c46c0
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 0 deletions.
18 changes: 18 additions & 0 deletions src/psygnal/_evented_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
NamedTuple,
Set,
Type,
TypeGuard,
Union,
cast,
no_type_check,
Expand All @@ -32,6 +33,7 @@
from pydantic import ConfigDict
from pydantic._internal import _model_construction as pydantic_main
from pydantic._internal import _utils as utils
from pydantic._internal._decorators import PydanticDescriptorProxy
from typing_extensions import dataclass_transform as dataclass_transform # py311

from ._signal import SignalInstance
Expand Down Expand Up @@ -133,6 +135,15 @@ def _get_fields(cls: pydantic.BaseModel) -> Dict[str, pydantic.fields.FieldInfo]
def _model_dump(obj: pydantic.BaseModel) -> dict:
return obj.model_dump()

def _is_pydantic_descriptor_proxy(obj: Any) -> TypeGuard["PydanticDescriptorProxy"]:
if (
type(obj).__module__.startswith("pydantic")
and type(obj).__name__ == "PydanticDescriptorProxy"
and isinstance(getattr(obj, "wrapped", None), property)
):
return True
return False

else:

@no_type_check
Expand Down Expand Up @@ -171,6 +182,9 @@ def _get_fields(cls: type) -> Dict[str, FieldInfo]:
def _model_dump(obj: pydantic.BaseModel) -> dict:
return obj.dict()

def _is_pydantic_descriptor_proxy(obj: Any) -> TypeGuard["PydanticDescriptorProxy"]:
return False


class ComparisonDelayer:
"""Context that delays before/after comparisons until exit."""
Expand Down Expand Up @@ -259,10 +273,14 @@ def __new__(
# in EventedModel.__setattr__
cls.__property_setters__ = {}
if allow_props:
# inherit property setters from base classes
for b in reversed(cls.__bases__):
if hasattr(b, "__property_setters__"):
cls.__property_setters__.update(b.__property_setters__)
# add property setters from this class
for key, attr in namespace.items():
if _is_pydantic_descriptor_proxy(attr):
attr = attr.wrapped
if isinstance(attr, property) and attr.fset is not None:
cls.__property_setters__[key] = attr
recursion = emission_map.get(key, default_strategy)
Expand Down
23 changes: 23 additions & 0 deletions tests/test_evented_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -939,3 +939,26 @@ class Config:
else:
assert m.events.a._reemission == mode
assert m.events.b._reemission == mode


@pytest.mark.skipif(not PYDANTIC_V2, reason="computed_field added in v2")
def test_computed_field() -> None:
from pydantic import computed_field

class MyModel(EventedModel):
a: int = 1
b: int = 1

@computed_field
@property
def c(self) -> List[int]:
return [self.a, self.b]

@c.setter
def c(self, val: Sequence[int]) -> None:
self.a, self.b = val

model_config = {
"allow_property_setters": True,
"field_dependencies": {"c": ["a", "b"]},
}

0 comments on commit 56c46c0

Please sign in to comment.