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

Refactor TraitList / TraitListObject #989

Merged
merged 28 commits into from
Apr 9, 2020
Merged

Conversation

mdickinson
Copy link
Member

@mdickinson mdickinson commented Apr 7, 2020

Here's the promised TraitList / TraitListObject refactor that I suggested last week. The main change is that whole-list validation is gone from TraitList: instead of the previous validator argument, TraitList now takes an item_validator argument, and that argument is used to validate any items being added in a list operation.

In meetings last week we discussed the possibility of a subclass of TraitList implementing the length-constraint logic. It turned out to be simplest to merge that proposed subclass with TraitListObject. So this PR doesn't introduce any new classes: we still have TraitList and TraitListObject, but length validation has moved into TraitListObject.

Rationale for the change:

  • It's not obvious that the validator API will extend naturally to future use-cases (for example, maintaining whole-list order); this feels like a case of premature abstraction. The only common use-case we have is item validation. The uncommon use-case we have is compatibility with minlen and maxlen support for the List trait type.
  • If we do have some form of whole-list validation in the future, that validation could involve needing to alter elements of the list other than added, and the current API doesn't support this.
  • We're not currently calling validator consistently: for example, it's not called for the sort and reverse methods, and without concrete use-cases it's hard to see what it should do in those cases.
  • The implementation and list operation logic in TraitList becomes simpler and clearer if we only need to validate new items.
  • Consistency with TraitSet and TraitDict, which don't do whole-object validation.
  • The validator API is likely to be awkward to use for consumers: for list length validation it works fine, but for other whole-list validation most uses would likely involve reconstructing the list from index, removed and added, which would be painful and bug-prone.
  • Long term, it may make sense to remove the minlen and maxlen support for List traits altogether; at that point, TraitListObject would become significantly simpler.

Key points:

  • TraitList now only knows about validating individual items, instead of validating the entire list.
  • Min/max length-checking has moved into TraitListObject.
  • TraitList operations always call the corresponding list operation, to maintain exception wording.
  • Despite earlier discussions, elements being added to a list are validated before the underlying list operation is called. That means that a validation error will take precedence over a Python IndexError: for example, given foo = List(Int), if myobj.foo has length 3, then myobj.foo[10] = 4.5 will raise a TraitError because 4.5 is not a valid int, rather than IndexError because 10 is out of range. This seems to be the most convenient and natural implementation-wise, and matches the current behaviour on master.
  • There's an odd corner case in TraitListObject.__setitem__: for an assignment involving a step that's neither 1 nor None, Python requires that the object assigned be the same length as the items it's replacing. However, there are tests that expect that the resulting ValueError takes precedence over any TraitError arising from invalid items, so there's some extra backwards compatibility logic in TraitListObject.__setitem__ to ensure those tests pass.

This isn't quite ready for merge: there's still some work to do in improving documentation and increasing coverage; while the trait_list_object module has been almost completely rewritten, I've deliberately tried to leave the tests as unchanged as possible, but I'd like to add more tests to ensure we have good coverage.

I'm making the PR now to get feedback from CI and from reviewers.

Refactor the relationship between TraitList and TraitListObject:

- TraitList now only knows about validating individual items,
  instead of validating the entire list.
- Min/max length-checking has moved into TraitListObject.
- TraitList operations always call the corresponding list
  operation, to maintain exception wording.
@mdickinson mdickinson added this to the 6.1.0 release milestone Apr 7, 2020
@mdickinson mdickinson added type: refactor topic: traits listener rework Issues related to reworking listener infrastructure; see also EEP2, EEP3 labels Apr 7, 2020
@codecov-io
Copy link

codecov-io commented Apr 7, 2020

Codecov Report

Merging #989 into master will increase coverage by 1.04%.
The diff coverage is 95.97%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #989      +/-   ##
==========================================
+ Coverage   73.05%   74.10%   +1.04%     
==========================================
  Files          51       51              
  Lines        6514     6453      -61     
  Branches     1309     1277      -32     
==========================================
+ Hits         4759     4782      +23     
+ Misses       1363     1305      -58     
+ Partials      392      366      -26     
Impacted Files Coverage Δ
traits/ctrait.py 61.98% <ø> (+3.42%) ⬆️
traits/has_traits.py 71.31% <ø> (+0.07%) ⬆️
traits/trait_base.py 62.98% <ø> (-0.60%) ⬇️
traits/trait_types.py 71.72% <88.88%> (+0.07%) ⬆️
traits/trait_list_object.py 97.48% <97.68%> (+23.20%) ⬆️
traits/trait_handlers.py 61.11% <100.00%> (ø)
traits/editor_factories.py 79.59% <0.00%> (-9.19%) ⬇️
traits/trait_type.py 77.21% <0.00%> (-1.10%) ⬇️
... and 3 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bac9f7b...75ef25b. Read the comment docs.

For __iadd__ and extend, we need to make sure that we validate
_all_ incoming items before we append any of them, else we
risk both modifying the list _and_ raising. So replace
the corresponding map() calls with list(map()), and convert
to a list comprehension for readability.

Also converts one other occurrence of list(map()) to a list
comprehension, with no change in semantics.
@mdickinson mdickinson marked this pull request as ready for review April 8, 2020 08:12
@kitchoi
Copy link
Contributor

kitchoi commented Apr 8, 2020

From discussion on another channel, this is ready for review and I am assigning myself (unless someone asks me wait).

@kitchoi kitchoi self-assigned this Apr 8, 2020
@mdickinson mdickinson requested a review from kitchoi April 8, 2020 10:59
Copy link
Contributor

@kitchoi kitchoi left a comment

Choose a reason for hiding this comment

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

Thank you! The TraitList does look simpler without having to validate the whole thing.
Some nitpicky comments.

traits/trait_list_object.py Show resolved Hide resolved
traits/trait_list_object.py Show resolved Hide resolved
traits/trait_list_object.py Show resolved Hide resolved
traits/trait_list_object.py Outdated Show resolved Hide resolved
traits/trait_list_object.py Outdated Show resolved Hide resolved
traits/tests/test_trait_list_object.py Outdated Show resolved Hide resolved
traits/tests/test_trait_list_object.py Outdated Show resolved Hide resolved
traits/trait_list_object.py Outdated Show resolved Hide resolved
@mdickinson
Copy link
Member Author

@kitchoi Thanks for the thorough review. I think I've addressed all comments.

@mdickinson mdickinson requested a review from kitchoi April 8, 2020 17:09
@mdickinson
Copy link
Member Author

mdickinson commented Apr 8, 2020

I've also just removed an XXX comment about documenting notification contents. I'm still not sure what to do about that, but I'll open an issue so it doesn't get forgotten.


EDIT: see #997

Copy link
Contributor

@kitchoi kitchoi left a comment

Choose a reason for hiding this comment

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

LGTM. Thank you!

@mdickinson
Copy link
Member Author

@midhun-pm Is it okay to merge this, or would like to review before it gets merged?

@midhun-pm
Copy link
Contributor

@mdickinson I haven't done a thorough review, but am referring to it as I make corresponding changes to TraitSet and TraitDict. These changes are making the code a lot more easier to understand. I don't want to keep it held up, so will open a PR if I find something that needs to be changed.

item_validator : callable, optional
Called to validate and/or transform items added to the list. The
callable should accept a single item from the list and return
the transformed item, raising TraitError for invalid items. If
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it necessary to ensure that the user defined validator raises a TraitError and not something else ? Do we need code to transform the error type if it doesn't?

Copy link
Member Author

Choose a reason for hiding this comment

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

What would code to transform the error type look like? We definitely don't want to catch all exceptions here, for the usual reasons (silencing genuine errors, making debugging harder). So would you target particular exception types? If so, which ones, and why?

For this sort of API, where you're delegating responsibility for validation to user code, you want to give that user code a well-defined way to say "nope, this item isn't valid", so that the "invalid" message can be safely distinguished from other messages, like coding errors.

And one clean way to do that is to provide a dedicated exception to use for that; in this case, TraitError.

@@ -212,471 +156,290 @@ def notify(self, index, removed, added):
for notifier in self.notifiers:
notifier(self, index, removed, added)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we catch errors raised by the user defined notifier so that the operation doesn't fail even if the notifier errors out ? There are cases where we return a value after calling notify and this would fail if any notification fails.

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 good question, though I think it's bigger than just this one PR. It's possible that we want to catch all failed user notifiers and turn the failures into logged warnings. But that's dangerous: it risks masking coding errors, and we also have internal notifiers to worry about (in fact, that's most of the use-case right now).

So yes, right now we should get a loud failure (an exception and traceback) if any notification fails, and right now that's what we want, since a failed notification likely represents a coding error. (We may still want to be catching exceptions inside notifiers at the point where they call into user callbacks; I think @kitchoi 's work still does that.)

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, an imminent PR for EEP 3 is going to do the exception handling inside the notifiers.

In addition to the reasons Mark mentioned, this is consistent with how ctraits call_notifiers work (so the notifiers will have to do their error handling anyway). I did not appreciate this earlier but I do now: This allows fine grained control on error handling later in the notifiers. The notifiers do a pile of things before it calls the user-provided handler. We'd want to do a catch-all around the user's handler that is outside traits control, but not around code that belongs to traits.

@mdickinson mdickinson merged commit b0c013d into master Apr 9, 2020
@mdickinson mdickinson deleted the trait-list-refactor branch April 9, 2020 07:13
@mdickinson
Copy link
Member Author

Merging; post-review comments still welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: traits listener rework Issues related to reworking listener infrastructure; see also EEP2, EEP3 type: refactor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants