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

Cross Validation Using New Decorator API #73

Merged
merged 19 commits into from
Sep 12, 2015

Conversation

rmorshea
Copy link
Contributor

brief example

from traitlets import HasTraits, Int, validate

class H(HasTraits):

    i = Int()

    @validate('i')
    def _i_validator(self, proposal):
        return proposal['value']+1

h  = H()
h.i = 1
print('result : '+ str(h.i))

'result : 2'

@SylvainCorlay SylvainCorlay mentioned this pull request Aug 28, 2015
@rmorshea rmorshea force-pushed the observe_validate branch 2 times, most recently from b580caa to 4a2fcc7 Compare August 28, 2015 20:52
@jasongrout
Copy link
Member

Like mentioned elsewhere, we will have problems if we have two different validators because validators also can coerce the argument to make it valid. Having the validator be a magic name enforces only one validator/coercer. Another option is to have the decorator fail the second time it is used to register a validator (I'm not sure if I like that any better, though...).

@SylvainCorlay
Copy link
Member

A small detail: having an event attribute to the descriptor, later used by the common instance-init is not very nice IMO (switching behaviors based on the value of a string).
Since you use inheritance, you may as well specialize instance_init in both implementations of the descriptors, and remove the code handling the case of an invalid string.

@rmorshea
Copy link
Contributor Author

rmorshea commented Sep 1, 2015

@SylvainCorlay let me know if that last commit addresses your concern. @jasongrout I think I'm going to open up a new branch based on this one, that will try to address the organization that these decorators should take on (and hopefully in the process, solve the problem of chaining validators).


@staticmethod
def register_event(inst, method, name):
getattr(inst, 'observe')(method, name)
Copy link
Member

Choose a reason for hiding this comment

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

inst.observe(method, name) ?

@rmorshea
Copy link
Contributor Author

rmorshea commented Sep 1, 2015

@SylvainCorlay or @jasongrout, are you guys available to chat briefly?

for name in self.names:
inst.observe(types.MethodType(self.func, inst), name=name)
self.register_event(ints, meth, name)
Copy link
Member

Choose a reason for hiding this comment

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

typo ints

@rmorshea rmorshea force-pushed the observe_validate branch 3 times, most recently from 6f07c4b to 7ebe0a0 Compare September 2, 2015 01:10
@@ -649,14 +697,23 @@ def observe(*names):
"""
return ObserveHandler(names)

def validate(name):
""" A decorator which can be used to cross validate a member on a class.
Copy link
Member

Choose a reason for hiding this comment

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

Let's not use the word "cross validate". How about "A decorator for validating the state of an HasTraits object when a Trait is set." or something like that.

"""Setup a handler to be called when a trait should be cross valdiated.

This is used to setup dynamic notifications for cross validation.
Cross validation handlers chain, meaning that the output of a prior
Copy link
Member

Choose a reason for hiding this comment

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

We don't have chaining anymore, right?

@jasongrout
Copy link
Member

Do we have tests to go along with this?

@rmorshea
Copy link
Contributor Author

rmorshea commented Sep 2, 2015

@jasongrout I was just using the same language as in 'observe', so I'll make that edit on both. And no tests yet, but I'll write those after lunch.

if isinstance(cb, types.MethodType):
nargs -= 1
if nargs == 0:
value = cb()
Copy link
Member

Choose a reason for hiding this comment

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

I don't think it makes sense to have a validator that takes no arguments. How will it know what value to return, if it has no idea what was set? I think validators should always take one argument, this change dictionary you created above.

@jasongrout
Copy link
Member

@rmorshea - thanks. The observe should probably also use Trait instead of member. @SylvainCorlay, what do you think?

@rmorshea
Copy link
Contributor Author

rmorshea commented Sep 2, 2015

To the first, good point, on the other, I figured it would be best to prevent people from trying to pass in multiple attribute names, but since observe is *names they'd probably get an error for too many arguments instead. I'll take that out.

@SylvainCorlay
Copy link
Member

This looks good to me.

@rmorshea
Copy link
Contributor Author

rmorshea commented Sep 9, 2015

I'm making these api updates to my matplotlib branch that uses traitlets and the callback signature of callback(self, change) is feeling a bit clunky because using change['name'] and change['new'] etc. is too verbose.

Because there isn't an easy way to enumerate the change dict (since it isn't ordered) I'm tempted to gravitate towards a naming scheme like callback(self, c) to clean that up, however c isn't particularly descriptive.

It might be nicer to make the signature callback(self, **change) for the cases where change['name'] isn't too bulky to use, and enumerating the dictionary into its corresponding variables is the only reasonable thing to do.

@SylvainCorlay
Copy link
Member

I recall that we discussed this with Chris and Jason and the choice for not expanding the dictionary came from the fact that it is future-proof, in that we will be able to add extra arguments to observe in the future if needed. Chris might have had other reasons to do the same in Atom.

@rmorshea
Copy link
Contributor Author

rmorshea commented Sep 9, 2015

So then just go with callback(self, c) I guess?

@ssanderson
Copy link
Member

@SylvainCorlay sadly (or maybe not...) that doesn't work in functions:

In [1]: def foo(x):
   ...:     locals().update(x)
   ...:     return y
   ...:

In [2]: foo({'y': 1})
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-2-48bdc5fe6135> in <module>()
----> 1 foo({'y': 1})

<ipython-input-1-3cf55a7db847> in foo(x)
      1 def foo(x):
      2     locals().update(x)
----> 3     return y
      4

NameError: global name 'y' is not defined

It does work, however, in class bodies:

In [3]: class Foo(object):
   ...:     locals().update({'y': 1})
   ...:

In [4]: Foo().y
Out[4]: 1

@SylvainCorlay
Copy link
Member

Sorry I deleted my previous message by mistake that was saying locals().update(change) 😄

@rmorshea
Copy link
Contributor Author

rmorshea commented Sep 9, 2015

So then we're back to callback(self, c)?

@SylvainCorlay
Copy link
Member

I guess so. In any case, this PR is about the validation decorator. To change back to **change, we will need to get everyone around the table again on that decision...

method = types.MethodType(self.func, inst)
return method(*args, **kwargs)

def __call__(self, *args, **kwargs):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

old __call__ method forced the use of super to access overwritten methods. 9ea2afa enables this syntax:

from traitlets import HasTraits, Int, observe

class A(HasTraits):
    i, j = Int(), Int()
    @observe('i')
    def _i_changed(self, change):
        self.j = change['new']

class B(A):
    k = Int()
    @observe('i')
    def _i_changed(self, change):
        A._i_changed(self, change)
        self.k = change['new']

@ellisonbg
Copy link
Member

I think it is a good idea for this to work with and without super, but let's add tests for that.

@SylvainCorlay
Copy link
Member

@rmorshea I don't think that you need to create a MethodType in _func_call. You can simply call the unbound method since inst is passed as the first argument in *args. This would give the following for EventHandler:

class EventHandler(BaseDescriptor):

    def _init_call(self, func):
        self.func = func
        return self

    def __call__(self, *args, **kwargs):
        if hasattr(self, 'func'):
            return self.func(*args, **kwargs)
        else:
            return self._init_call(*args, **kwargs)

    def __get__(self, inst, cls=None):
        if inst is None:
            return self
        return types.MethodType(self.func, inst)

Besides, this would simplify the support of bare functions in the future.

@SylvainCorlay
Copy link
Member

@minrk @jasongrout @ellisonbg do you guys want to have another look at this?

@minrk
Copy link
Member

minrk commented Sep 11, 2015

@SylvainCorlay thanks, I'll try to have a look tomorrow.

@rmorshea rmorshea force-pushed the observe_validate branch 3 times, most recently from 7e1c41b to 8ce0f31 Compare September 11, 2015 21:28
@rmorshea
Copy link
Contributor Author

8ce0f31

  • Corrects bad pickling test which didn't test for when methods are added to the instance as notifiers.
  • Moves popping _notify_trait off __dict__ from the end of with_hold_trait_notifications to __getstate__ since the method needs to be there to handle the _trait_notifiers and _trait_validatators attributes anyway (consolidates pickling logic).
  • Introduces __setstate__ to reassign notifiers attached to the HasTraits instance by EventHandler instances during instance_init. (this does not resolve methods which were assigned via inst.observe or inst.validate)

@SylvainCorlay
Copy link
Member

@rmorshea let's hold off until this is merged and iterate in other PRs.

@rmorshea
Copy link
Contributor Author

@SylvainCorlay I'll make a separate pr.

@minrk
Copy link
Member

minrk commented Sep 12, 2015

@SylvainCorlay @rmorshea 👍 from me. I'm happy to merge if this gets OK from @ellisonbg.

@ellisonbg
Copy link
Member

Looks good, merging.

ellisonbg added a commit that referenced this pull request Sep 12, 2015
Cross Validation Using New Decorator API
@ellisonbg ellisonbg merged commit e698d58 into ipython:master Sep 12, 2015
@rmorshea rmorshea deleted the observe_validate branch September 20, 2015 19:17
@minrk minrk modified the milestone: 4.1 Nov 9, 2015
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.

None yet

6 participants