Optional accessors in Sources#4277
Conversation
There was a problem hiding this comment.
Thanks for the contribution!
I'm a new core team member, so I'm not going to approve or request changes directly, but I will comment.
I'm in two minds about whether "missing accessors" should be represented by [] or by None, but either way there is a mismatch in the default value vs. the value of self._accessors. Either:
- the default value in the
__init__should be[]or()or something like that (andNoneis not the default, nor allowed), andself._accessorsis then[]; or - the default value in the
__init__should beNone(and if an empty sequence is given, it is converted toNone) andself._accessorsis set toNone
The empty list has a certain elegance, but None may be more Pythonic.
| elif hasattr(data, "__iter__") and not isinstance(data, str): | ||
| if len(self._accessors) == 0: | ||
| raise ValueError( | ||
| "TreeSource requires accessors for non-mapping node data" | ||
| ) | ||
| node = Node(**dict(zip(self._accessors, data, strict=False))) | ||
| else: | ||
| if len(self._accessors) == 0: | ||
| raise ValueError( | ||
| "TreeSource requires accessors for non-mapping node data" | ||
| ) |
There was a problem hiding this comment.
It might be nicer to have the logic look more like:
elif len(self._accessors) > 0:
if hasattr(....) ...:
node = Node(...)
else:
node = Node(...)
else:
raise ValueError(...)(and similarly in the ListSource).
This is not a required change.
Not sure if I'm misunderstanding your intention here, but the value of a default argument absolutely must not be However, in this case, I'd be inclined to say that "there are no accessors" should be represented as |
|
I have a question about this. I used the data: Iterable | None = NoneThen it's initialized like this: if data is not None:
self._data = [self._create_row(value) for value in data]
else:
self._data = []So when |
Apologies for the confusion here. There's one more typing detail that (hopefully) clarifies what is going on here. You've identified the type declaration of However, there's also a type declaration on L108: This is because of the detail I flagged in my previous review- we cannot use This code code reads as if the "arg" argument has a default value of [] (and None is not an allowed value). A simple reading of that code would lead you to thing that it would print "first value=[42]" then "second value=[42]" (as both calls use the "default" argument value). However, you actually get "second value=[42, 42, 42]". This is because the value of the default is evaluated once, when the function is parsed; as a result, the same list instance is used as a default every time the function is invoked. This is true for any mutable data type. So - a general pattern if you want "default value is a list" (or any other mutable data type) is to accept a value of None, and then convert that value of None into the "actual" value that you want. In this case: So - that's how However - Does that make any more sense? |
d221195 to
590f1eb
Compare
|
@Pulga8 Is this ready for a re-review, or are you planning to make additional changes? |
|
Yes, it's ready. I believe I've interpreted the request correctly. Apologies if I misunderstood, and thank you for your patience. |
freakboy3742
left a comment
There was a problem hiding this comment.
Thanks - this looks great. The only issue of any consequence is the exact handling of accessors = [] case; as I've noted inline, it's a simpler implementation and more internally consistent to allow an empty list, even though it's a weird edge case.
I've also tweaked the documentation and error messages; the documentation was an accurate description of what was being done in the implementation, but didn't really provide the guidance needed as a user of the API.
Thanks for the PR!
|
|
||
| def __init__(self, accessors: Iterable[str], data: Iterable | None = None): | ||
| def __init__( | ||
| self, accessors: Iterable[str] | None = None, data: Iterable | None = None |
There was a problem hiding this comment.
A minor code style issue - we try to avoid the "all args on one separate line" format, even though it's legal from Ruff's perspective. We prefer to go directly to "one arg per line":
| self, accessors: Iterable[str] | None = None, data: Iterable | None = None | |
| self, | |
| accessors: Iterable[str] | None = None, | |
| data: Iterable | None = None |
| in each column of the row. If omitted, only mapping row data can be | ||
| converted. |
There was a problem hiding this comment.
| in each column of the row. If omitted, only mapping row data can be | |
| converted. | |
| in each column of the row. If omitted, row data must be specified as a | |
| mapping. |
| accessors = list(accessors) | ||
| self._accessors = accessors if len(accessors) > 0 else None |
There was a problem hiding this comment.
It's an edge case; but for consistency, I'd argue an empty list should be interpreted as a list of no accessors, rather than "accessors haven't been provided". That would technically allow construction of "empty" Row objects from list-like data. That isn't really useful, but it's internally consistent (and the implementation is simpler as well).
| accessors = list(accessors) | |
| self._accessors = accessors if len(accessors) > 0 else None | |
| self._accessors = list(accessors) |
| else: | ||
| # Copy the list of accessors, in case of [] its None. | ||
| accessors = list(accessors) | ||
| self._accessors = accessors if len(accessors) > 0 else None |
There was a problem hiding this comment.
As with list source; this can be simplified for consistency.
| Data sources are abstractions that allow you to define the data being managed by your application independent of the GUI representation of that data. For details on the use of data sources, see the [topic guide](/topics/data-sources.md). | ||
|
|
||
| ListSource is an implementation of an ordered list of data. When a ListSource is created, it is given a list of `accessors` - these are the attributes that all items managed by the ListSource will have. The API provided by ListSource is [`list`][]-like; the operations you'd expect on a normal Python list, such as `insert`, `remove`, `index`, and indexing with `[]`, are also possible on a ListSource: | ||
| ListSource is an implementation of an ordered list of data. When a ListSource is created, it is given a list of `accessors` - these are the attributes that all items managed by the ListSource will have. If no accessors are provided, or an empty sequence is given, the source stores `None`. The API provided by ListSource is [`list`][]-like; the operations you'd expect on a normal Python list, such as `insert`, `remove`, `index`, and indexing with `[]`, are also possible on a ListSource: |
There was a problem hiding this comment.
This is an accurate description of the implementation, but that doesn't describe how or why you'd use a non-accessor ListSource. The docs should be focussed on usage.
|
Thanks for your re-review, freakboy3742, and for the helpful feedback! |
Fixes #4221
This PR updates ListSource and TreeSource so accessors are optional when the input data is mapping-based (for example, dictionaries). Previously, accessors were effectively required for all non-Row inputs, which made mapping-based usage more restrictive than necessary.
Changes:
PR Checklist: