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

Fix repeated Property triggering in subclasses #1587

Merged
merged 4 commits into from
Oct 26, 2021

Conversation

mdickinson
Copy link
Member

This PR is a minimal, non-invasive fix for #1586, suitable for inclusion in a bugfix release. I (just barely) resisted the temptation to do a more significant refactoring of update_traits_class_dict.

Explanation of the fix: update_traits_class_dict is invoked by the MetaHasTraits machinery for each HasTraits subclass that's declared. It roughly proceeds in three stages:

  • Stage 1: scan the dictionary of the HasTraits subclass being built, looking for trait definitions, Property definitions, and on_trait_change and observe-decorated methods
  • Stage 2: merge information from the immediate base classes of the class being built
  • Stage 3: perform additional hookups for various things, including listener information for Property depends_on and observe metadata

In the situation of #1586, when processing TestSubclass, stage 2 brought in the foo listener for the base class's Property, and then stage 3 added another, identical listener.

This PR fixes the logic to match what's done for on_trait_change: there, stage 2 brings in the base class listener, and then stage 3 overwrites that listener with a new one. For observe, stage 3 is extending rather than overwriting, and this PR changes the logic so that it overwrites.

Finally, this PR also makes two non-behavioural changes for consistency and clarity: the fix above introduces the situation that we're sometimes extending the list of observers for a given name and sometimes overwriting. But in both stage 1 and stage 2 I believe it's the case that the list of observers being extended must be the empty list, so we can replace those with a simple assignment.

I'm failing to find a use-case for the original logic that extended the list of observers.

Fixes #1586.

@@ -559,8 +559,7 @@ def update_traits_class_dict(class_name, bases, class_dict):

observer_states = getattr(value, "_observe_inputs", None)
if observer_states is not None:
stack = observers.setdefault(name, [])
stack.extend(observer_states)
observers[name] = observer_states
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a minor consistency cleanup that should not affect behaviour: observer was created fresh as an empty dictionary ahead of the for loop containing this code; assignment to any given name can only occur once for that name during execution of the for loop.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean observers - not observer - the empty dictionary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, yes: observers.

if name not in class_traits and name not in class_dict:
stack = observers.setdefault(name, [])
stack.extend(states)
if (name not in class_traits) and (name not in class_dict):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this should not affect behaviour: the only way for name to already be present in observers is for it to have been added by the for loop that processes class_dict, but in that case the name not in class_dict condition will fail and this code will not be reached.

To double check the reasoning, I added an assert name not in observers to the old code and ran the entire test suite; that assert wasn't triggered.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Looking more closely at this, there is actually a behaviour change here: the old code makes a (shallow) copy of the observer states, while this code just refers to the same list.

I could change this to states.copy(), but I don't think that change is necessary: I'm fairly sure that we don't mutate the states anywhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't mutate the states anywhere

That should say: "we don't mutate the list of states anywhere". (I don't think we mutate the state dictionaries themselves either, but that's not relevant here.)

@@ -758,8 +756,7 @@ def update_traits_class_dict(class_name, bases, class_dict):
property_name=name,
cached=trait.cached,
)
stack = observers.setdefault(name, [])
stack.append(observer_state)
observers[name] = [observer_state]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is the one that introduces a behaviour change and fixes the issue.

@@ -559,8 +559,7 @@ def update_traits_class_dict(class_name, bases, class_dict):

observer_states = getattr(value, "_observe_inputs", None)
if observer_states is not None:
stack = observers.setdefault(name, [])
stack.extend(observer_states)
observers[name] = observer_states
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean observers - not observer - the empty dictionary?

@mdickinson mdickinson merged commit c903296 into main Oct 26, 2021
@mdickinson mdickinson deleted the fix/repeated-property-observer branch October 26, 2021 12:40
mdickinson added a commit that referenced this pull request Oct 26, 2021
@mdickinson
Copy link
Member Author

#1588 is a backport of this PR to the 6.3 maintenance branch.

@mdickinson mdickinson added porting: backported to 6.3 This PR has been backported to the Traits 6.3.x release branch and removed porting: needs backport to 6.3 labels Oct 26, 2021
mdickinson added a commit that referenced this pull request Oct 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
porting: backported to 6.3 This PR has been backported to the Traits 6.3.x release branch
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Property traits with observe get called twice in subclasses
2 participants