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

Make Tuple use a dynamic default when necessary #1521

Merged
merged 2 commits into from
Sep 13, 2021

Conversation

mdickinson
Copy link
Member

@mdickinson mdickinson commented Sep 10, 2021

This PR fixes the Tuple trait type so that it only uses a constant default value when all of its component traits can use a constant default value. This includes simple cases like Tuple(Int(), Str()) as well as nested cases like Tuple(Int(), Tuple(Int(), Str())).

Fixes #1520.

# Dynamic default, used when at least one of the child traits requires
# a dynamic default.
return tuple(
inner_trait.default_value_for(object, "<inner_trait>")
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 part's annoying: we have to invent a name because default_value_for expects one, but the DefaultValue.callable default type doesn't pass any name along. So I chose something that should at least be non-ambiguous and can easily be searched for in the Traits source.

I couldn't find any way to actually get the name to show up in validation error messages. For example with List child traits, at the point where this dynamic default is used, it's then validated and at the validation step the TraitList objects are recreated with name matching the name of the actual trait; those recreated TraitList objects are what are actually used in the trait value.

@@ -2283,16 +2283,40 @@ def __init__(self, *types, **metadata):
if len(types) == 0:
types = [Trait(element) for element in default_value]

self.types = tuple([trait_from(type) for type in types])
self.types = tuple(trait_from(type) for type in types)
Copy link
Member Author

Choose a reason for hiding this comment

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

Gratuitous drive-by style cleanup.

traits/trait_types.py Outdated Show resolved Hide resolved
self.init_fast_validate(ValidateTrait.tuple, self.types)

if default_value is None:
default_value = tuple(
[type.default_value()[1] for type in self.types]
# Optimisation: if all child traits have a constant default value,
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. This isn't actually purely optimisation: we don't validate defaults in the static case, so in current Traits we can have for example:

>>> from traits.api import *
>>> class A(HasTraits):
...     foo = Tuple(Int("some string"), Str(5))
... 
>>> a = A()
>>> a.foo
('some string', 5)

The "optimisation" here ensures that this behaviour (or arguably misbehaviour) persists.

Copy link
Contributor

@corranwebster corranwebster left a comment

Choose a reason for hiding this comment

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

Looks good. One suggestion, but I don't know if it is an improvement or not.

Comment on lines +2293 to 2303
child_defaults = []
child_default_types = []
for child_trait in self.types:
child_default_type, child_default = child_trait.default_value()

child_default_types.append(child_default_type)
child_defaults.append(child_default)

constant_default = all(
dvt == DefaultValue.constant for dvt in child_default_types
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Short-circuit the loop if you hit a non-constant default

Suggested change
child_defaults = []
child_default_types = []
for child_trait in self.types:
child_default_type, child_default = child_trait.default_value()
child_default_types.append(child_default_type)
child_defaults.append(child_default)
constant_default = all(
dvt == DefaultValue.constant for dvt in child_default_types
)
child_defaults = []
constant_default = True
for child_trait in self.types:
child_default_type, child_default = child_trait.default_value()
if child_default_type != DefaultValue.constant:
constant_default = False
break
child_defaults.append(child_default)

or something like this?

Copy link
Member Author

Choose a reason for hiding this comment

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

I actually already did this, and then changed to the current implementation on the basis that it was clearer and that the loop seemed unlikely to be a bottleneck.

Copy link
Member Author

Choose a reason for hiding this comment

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

(Off-topic, but turning a list of pairs into a pair of lists seems unnecessarily awkward in Python - it feels as though it ought to be a one-liner.)

@mdickinson mdickinson merged commit c69bbe4 into main Sep 13, 2021
@mdickinson mdickinson deleted the fix/trait-dynamic-default branch September 13, 2021 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Tuple has bad defaults for some inner trait types (for example, List)
2 participants