Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specify the name of a single changed signal in EventedModel #261

Closed
getzze opened this issue Feb 1, 2024 · 4 comments
Closed

Specify the name of a single changed signal in EventedModel #261

getzze opened this issue Feb 1, 2024 · 4 comments

Comments

@getzze
Copy link
Contributor

getzze commented Feb 1, 2024

My problem, I am using attrs to make a dataclass with a name attribute.
I want to modify the name before setting it, but I cannot use attrs.converters because they are
not bound to the instance. So I create a private _name field and use a name property to validate the name
before setting it.
Now I would like to emit a signal when name is changed. This is my working example using PR #260.

Now the signal is emitted at self.events._name_changed but I would like it to be self.events.name_changed or self.events.name, i.e. without the "_" prefix because then I will add other signals like self.events.color and I want it to be consistent, so all without the "_" prefix.

Any idea how I can do that without another PR :) ?

from typing import ClassVar
from attrs import define, field
from psygnal import SignalGroupDescriptor, EmissionInfo


@define
class Model:
    events: ClassVar[SignalGroupDescriptor] = SignalGroupDescriptor(signal_suffix="_changed")

    _controller: str = field()
    _name: str = field(kw_only=True)

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, value: str) -> None:
        # Generate a unique name among siblings
        siblings = [self.controller]
        value = f"{value}_1" if value in siblings else value
        self._name = value

    @property
    def controller(self) -> str:
        return self._controller

m = Model("ctt", name="c")
@m.events.connect
def on_any_change(info: EmissionInfo):
    print(f"field {info.signal.name!r} changed to {info.args}")

m.name = "ctt"; m.name
# >> field '_name_changed' changed to ('ctt_1', 'c')
# I would like it to be 'name_changed'
@getzze
Copy link
Contributor Author

getzze commented Feb 1, 2024

I managed to make it work with a custom signal_group_class argument. I have to see how it behaves with sub-classing though.

from typing import ClassVar
from attrs import define, field
from psygnal import SignalGroupDescriptor, Signal, SignalGroup, EmissionInfo

ModelSignalGroup = type("ModelSignalGroup", (SignalGroup,), {"name": Signal(str, str)})

@define
class Model:
    events: ClassVar[ModelSignalGroup] = SignalGroupDescriptor(
        signal_group_class= ModelSignalGroup,
    )

    _controller: str = field()
    _name: str = field(kw_only=True)

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, value: str) -> None:
        # Generate a unique name among siblings
        siblings = [self.controller]
        value = f"{value}_1" if value in siblings else value
        self._name = value

    @property
    def controller(self) -> str:
        return self._controller

m = Model("ctt", name="c")
@m.events.connect
def on_any_change(info: EmissionInfo):
    print(f"field {info.signal.name!r} changed to {info.args}")

m.name = "ctt"; m.name
# >>> field 'name' changed to ('ctt_1', 'c')

@getzze
Copy link
Contributor Author

getzze commented Feb 1, 2024

It works with sub-classing, although I have to redefine the events class attribute for the subclasses.

@tlambert03
Copy link
Member

see #262 and #260 (comment) for another option.

@getzze
Copy link
Contributor Author

getzze commented Mar 22, 2024

This can be closed, with #299 merged, it can be done in two ways (that are not exactly equivalent).

With aliases only:

from typing import ClassVar
from attrs import define, field
from psygnal import SignalGroupDescriptor, EmissionInfo

aliases = {"_name": "name", "name": None, "_controller": None}

@define
class Model:
    events: ClassVar[SignalGroupDescriptor] = SignalGroupDescriptor(signal_aliases=aliases)

    _controller: str = field()
    _name: str = field(kw_only=True)

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, value: str) -> None:
        # Generate a unique name among siblings
        siblings = [self.controller]
        value = f"{value}_1" if value in siblings else value
        self._name = value

    @property
    def controller(self) -> str:
        return self._controller

m = Model("ctt", name="c")
@m.events.connect
def on_any_change(info: EmissionInfo):
    print(f"field {info.signal.name!r} changed to {info.args}")

m.name = "ctt"; m.name
# >> field 'name' changed to ('ctt_1', 'c')

With aliases and a SignalGroup subclass (thanks to #291, it will work on new fields if subclassing Model without the need to redefine events in the subclass):

from typing import ClassVar
from attrs import define, field
from psygnal import SignalGroupDescriptor, Signal, SignalGroup, EmissionInfo

ModelSignalGroup = type("ModelSignalGroup", (SignalGroup,), {"name": Signal(str, str)})

@define
class Model:
    events: ClassVar[ModelSignalGroup] = SignalGroupDescriptor(
        signal_group_class= ModelSignalGroup,
        signal_aliases={"_name": None, "_controller": None}
    )

    _controller: str = field()
    _name: str = field(kw_only=True)

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, value: str) -> None:
        # Generate a unique name among siblings
        siblings = [self.controller]
        value = f"{value}_1" if value in siblings else value
        self._name = value

    @property
    def controller(self) -> str:
        return self._controller

m = Model("ctt", name="c")
@m.events.connect
def on_any_change(info: EmissionInfo):
    print(f"field {info.signal.name!r} changed to {info.args}")

m.name = "ctt"; m.name
# >>> field 'name' changed to ('ctt_1', 'c')

@getzze getzze closed this as completed Mar 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants