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

Allow iterables of qualified class names in Stream.__getitem__ searches #1359

Merged
merged 6 commits into from
Aug 12, 2022
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions music21/stream/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,13 +434,17 @@ def __getitem__(self, k: slice) -> t.List[M21ObjType]:
@overload
def __getitem__(
self,
k: t.Type[ChangedM21ObjType]
k: t.Union[t.Type[ChangedM21ObjType], t.Collection[t.Type[ChangedM21ObjType]]]
) -> iterator.RecursiveIterator[ChangedM21ObjType]:
Copy link
Member

Choose a reason for hiding this comment

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

does this properly type the output for mypy? (just want to be sure that that was checked)

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 think I'm not quite following -- can you explain what you mean? (mypy passes, and I didn't touch the return type)

Copy link
Member

Choose a reason for hiding this comment

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

What I mean is something like, say you start typing:

for el in s[note.Note, chord.Chord]:
    print(el.pitches)
    print(el.recurse())

will the el.pitches line start to auto-suggest .pitches when you've typed .pi... (like does it know that all possible members have .pitches in it), and will mypy choke on the el.recurse() -- knowing that the union of note.Note and chord.Chord does not have .recurse() defined on all members -- or even one member.

And if you do:

for el in s[note.Note, chord.Chord]:
    if not isinstance(el, note.Note):
         print(el.closedPosition())

and know that this is fine? I'm just wondering if the typing is inferred from the ChangedM21Type.

And most importantly, does this still raise a mypy error:

for el in s[note.Note]:
     print(el.isMajorTriad())

while this passes?

for el in s[note.Note]:
     print(el.pitch)

I'll just grab the PR and play with it. But I'm wondering if putting the two different types of things in the same @overload might cause problems.

x = t.cast(iterator.RecursiveIterator[ChangedM21ObjType], self.recurse())
return x # dummy code

def __getitem__(self,
k: t.Union[str, int, slice, t.Type[ChangedM21ObjType]]
k: t.Union[str,
int,
slice,
t.Type[ChangedM21ObjType],
t.Collection[t.Type[ChangedM21ObjType]]]
) -> t.Union[iterator.RecursiveIterator[M21ObjType],
iterator.RecursiveIterator[ChangedM21ObjType],
M21ObjType,
Expand Down Expand Up @@ -486,8 +490,8 @@ def __getitem__(self,
3.0


If a class is given then an iterator of elements
that match the requested class(es) is returned, similar
If a class is given, then a :class:`~music21.stream.iterator.RecursiveIterator`
of elements matching the requested class is returned, similar
to `Stream().recurse().getElementsByClass()`.

>>> len(s)
Expand All @@ -510,6 +514,18 @@ def __getitem__(self,
>>> len(s[note.Note])
7

Multiple classes can be provided, separated by commas. Any element matching
any of the requested classes will be matched.

>>> len(s[note.Note, note.Rest])
9

>>> for note_or_rest in s[note.Note, note.Rest]:
... if isinstance(note_or_rest, note.Note):
... print(note_or_rest.name, end=' ')
... else:
... print("Rest", end = ' ')
Copy link
Member

Choose a reason for hiding this comment

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

quotes and spacing. (Sorry that the new linter doesn't do doctests.

C C# D E Rest F G Rest A

The actual object returned by `s[module.Class]` is a
:class:`~music21.stream.iterator.RecursiveIterator` and has all the functions
Expand Down Expand Up @@ -556,7 +572,8 @@ def __getitem__(self,

>>> s[0.5]
Traceback (most recent call last):
TypeError: Streams can get items by int, slice, class, or string query; got <class 'float'>
TypeError: Streams can get items by int, slice, class, class iterable, or string query;
got <class 'float'>

Changed in v7:
- out of range indexes now raise an IndexError, not StreamException
Expand All @@ -573,6 +590,7 @@ def __getitem__(self,
.recurse().getElementsByClass to get the earlier behavior. Old behavior
still works until v9. This is an attempt to unify __getitem__ behavior in
StreamIterators and Streams.
- allowed iterables of qualified class names, e.g. `[note.Note, note.Rest]`
'''
# need to sort if not sorted, as this call may rely on index positions
if not self.isSorted and self.autoSort:
Expand Down Expand Up @@ -607,7 +625,10 @@ def __getitem__(self,

return t.cast(M21ObjType, searchElements[k])

elif isinstance(k, type) and issubclass(k, base.Music21Object):
elif isinstance(k, type):
return self.recurse().getElementsByClass(k)

elif common.isIterable(k) and all(isinstance(maybe_type, type) for maybe_type in k):
return self.recurse().getElementsByClass(k)

elif isinstance(k, str):
Expand All @@ -619,7 +640,8 @@ def __getitem__(self,
return querySelectorIterator

raise TypeError(
f'Streams can get items by int, slice, class, or string query; got {type(k)}'
'Streams can get items by int, slice, class, class iterable, or string query; '
f'got {type(k)}'
)

def first(self) -> t.Optional[M21ObjType]:
Expand Down