-
-
Notifications
You must be signed in to change notification settings - Fork 74
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
Use sentinel value for inheriting parameter slots #605
Conversation
Codecov Report
@@ Coverage Diff @@
## master #605 +/- ##
==========================================
- Coverage 81.95% 81.90% -0.06%
==========================================
Files 4 4
Lines 3020 3050 +30
==========================================
+ Hits 2475 2498 +23
- Misses 545 552 +7
Continue to review full report at Codecov.
|
|
Recording some issues. This snippet used to run fine and class P(param.Parameterized):
s = param.String(default=None, allow_None=False) On the branch in its current state running the same snippet raises an error:
This happens in the validation code path of class P(param.Parameterized):
p = param.Parameter(default=None, allow_None=False)
inst = P()
Interestingly, the test suite passes despite this change. (Yep and |
Replying to my own comment above #605 (comment). This is the same behavior as main, which is correct, with import param
class P(param.Parameterized):
s = param.String(default=None, allow_None=False)
inst = P()
print(repr(inst.s))
# None
print(inst.param.s.allow_None)
# True |
I think I am going to stop there for now :) Early on I felt that the test suite was not good enough to capture all the changes that would be brought by this PR. Metadata inheritance wasn't really tested much, if at all. Users could rely on the fact that it didn't work, without that being captured by the test suite. See for instance this case in HoloViews holoviz/holoviews#5667. I also observed that the test suite was lacking as I ran Panel's unit tests against this PR (after Jim's work) and it failed. So I decided to add a lot of tests! These tests were all added on main first and then merged in this PR. With obviously some informative title:
This included:
While all the added tests are important, the later ones in the list above are special for this PR, as some of them needed to be updated to reflect the new behavior. I would advise reviewers to carefully check these tests changes. WARNING Overall, I find that I haven't really managed to find the recipe to enable metadata inheritance in dynamic cases, i.e. when some metadata value depends on some others. It felt every time a little custom, it'd be great to be able to better abstract that.
This PR now raises a
Still on the TODO list, should be pretty straightforward though. We should probably also document how to write custom Parameters in a way that allows metadata inheritance for additional slots.
It'd be nice, there must be some edge cases though. For instance, should that valid? import param
class A(param.Parameterized):
p = param.ClassSelector(class_=(list, dict))
class B(param.Parameterized):
p = param.List() It should be pretty straightforward to run the test suite of Panel/HoloViews/etc. from a branch that raises an error if the Parameter from which a metadata value is inherited (in
I haven't had a look at it yet. It looks like it shouldn't be too hard, but it can definitely be implemented later, even after 2.0. A potentially important bit of information on a change made in this PR, I made I would like to stress something that might be overlooked by this PR but that I judge pretty important. Now the Parameter signatures no longer have some of the default attribute values (like
Some super quick and likely bad ideas include:
I have no clue about the performance impact of implementing Given the potential effects of this PR and the fact that the test suite is certainly not catching all the changes, I advise we run the complete test suite of the following repos from that branch:
(I have just run the unit test suite of Panel and HoloViews) EDIT: ran then excluding GeoViews and some examples tests, it went fine. The next test will be when an alpha version of Param gets released. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! I've made scattered comments that need addressing, but it seems like this is finally nearing the end!
[Updated description to match the currently implemented version, not the original commits in this PR, for which see the history.]
Addressing the parameter inheritance issues at #97 and #113 by distinguishing between "not set by the user" and "set by the user" (including for None), using a sentinel value
Undefined
distinct from None and other valid values. Notes:default
orbounds
). Previously, slot values specified as None in a Parameter constructor were inherited from parameters of the same name in superclasses, which fails when None is an allowed value, and also fails to inherit if default values are boolean or other non-None values. Now, inheritance checks for slot values of Undefined instead and if found inherits the value from superclass values._Undefined
, used internally only, but now renamed since it's part of the public API.0.0
or(False,True)
stored those defaults in the constructor's signature, but now that the signature shows onlyUndefined
, we have to store those values somewhere else. Added a dictionary_slot_defaults
to Parameter containing those values, used when inheritance "bottoms out" and no value has been specified by the user in a constructor at any level. Subclasses can extend this dictionary like_slot_defaults = dict_update(Parameter._slot_defaults, default=0.0, inclusive_bounds=(True,True))
to override or add new defaults. Several Parameter subclasses now no longer even need constructors, as the constructor previously existed solely to capture and declare those default values.Parameter.__getattribute__
:__param_inheritance
is applied when a Parameterized object is constructed, filling in slot values from superclasses. Note that slots are also used in "unbound" parameters not (yet) owned by any Parameterized. In particular, the Parameter constructor must complete before the Parameter can be bound into its owning Parameterized class, and so any code in the constructor must be able to run even though the Undefined slot values have not yet been filled in with their values. Specifically, the constructors typically validate the various slot values, and this validation needs to complete for construction to succeed. Previous commits either deferred validation until binding time (which broke many tests that assumed validation happened immediately) or explicitly wrapped every access to a slot with code that checks for Undefined and substitutes the default (which was highly verbose and only worked for None default values anyway). Now,Parameter.__getattribute__
is overridden to catch Undefined values and look them up in _slot_defaults (with None if not found there), so that slot values can be Undefined to allow inheritance without the programmer or user ever having to reason about that value.self.<slotvalue>
vs.<constructorarg>
. Previously, the various constructors referred fairly randomly either to the constructor argument (e.g.default
) or the slot value once it has been put into the slot (e.g.self.default
), since those were typically equal. The behavior of those two things now differs, when the value is Undefined --default is Undefined
, whileself.default is None
(or whatever default value was declared). So, each of the Parameter constructors has been updated as needed to useself.<slotvalue>
so that there is no need to handle Undefined, or else left as<constructorarg>
in the rare case when needing to check for Undefined explicitly.Fixes #97
Fixes #113.
With these changes, default values and nearly all other slots now respect None, which they did not do previously:
(prior to this PR, the output would have been
(5, 8, 2)
). They also handle cases like Number where the default is non-None:To do:
allow_None
andinstantiate
) in Parameter, Selector, ClassSelector, List, MultiFileSelector, Event, etc.). For most of these, the final value for the slot is calculated in the constructor based on other slot values, and thus can't be captured as a single static default value in the_slot_defaults
. I assume they'll either need another mechanism (calling a method for their value?) or to declare that they are not inherited.Required
for a user to use as the default value for a parameter and raising an error in Parameterized's constructor if that value is detected. I don't think that Undefined will work here, since most or all Parameters ultimately define a default value, and Undefined will eventually be replaced by that value.