Skip to content

Commit

Permalink
Merge pull request #1295 from cuthbertLab/iterator-mypy
Browse files Browse the repository at this point in the history
StreamCore is now a Music21Object; Iterator improvements
  • Loading branch information
mscuthbert authored May 6, 2022
2 parents 11dc9e6 + dd8685a commit 137bf09
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 157 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pythonpylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,5 @@ jobs:
- name: Type-check certain modules with mypy
run: |
mypy --follow-imports=silent music21/capella music21/common music21/corpus music21/features music21/figuredBass music21/humdrum music21/ipython21 music21/languageExcerpts music21/lily music21/mei music21/metadata music21/musedata music21/noteworthy music21/omr music21/romanText music21/test music21/vexflow
mypy --follow-imports=silent music21/stream/base.py music21/stream/core.py music21/stream/enums.py music21/stream/filters.py
mypy --follow-imports=silent music21/stream/base.py music21/stream/core.py music21/stream/enums.py music21/stream/filters.py music21/stream/iterator.py
mypy --follow-imports=silent music21/articulations.py music21/bar.py music21/base.py music21/beam.py music21/clef.py music21/configure.py music21/defaults.py music21/derivation.py music21/duration.py music21/dynamics.py music21/editorial.py music21/environment.py music21/exceptions21.py music21/expressions.py music21/freezeThaw.py music21/harmony.py music21/instrument.py music21/interval.py music21/key.py music21/layout.py music21/note.py music21/percussion.py music21/pitch.py music21/prebase.py music21/repeat.py music21/roman.py music21/serial.py music21/sieve.py music21/sites.py music21/sorting.py music21/spanner.py music21/style.py music21/tablature.py music21/tempo.py music21/text.py music21/tie.py music21/tinyNotation.py music21/variant.py music21/voiceLeading.py music21/volpiano.py music21/volume.py
8 changes: 4 additions & 4 deletions music21/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1780,7 +1780,7 @@ def contextSites(
This example shows the context sites for Measure 3 of the
Alto part. We will get the measure object using direct access to
indices to ensure that no other temporary
streams are created; normally, we would do `c.parts['Alto'].measure(3)`.
streams are created; normally, we would do `c.parts['#Alto'].measure(3)`.
>>> m = c[2][4]
>>> m
Expand Down Expand Up @@ -4385,7 +4385,7 @@ def testBeatAccess(self):
from music21 import stream

s = corpus.parse('bach/bwv66.6.xml')
p1 = s.parts['Soprano']
p1 = s.parts['#Soprano']

# this does not work; cannot get these values from Measures
# self.assertEqual(p1.getElementsByClass(stream.Measure)[3].beat, 3)
Expand Down Expand Up @@ -4483,7 +4483,7 @@ def testMeasureNumberAccess(self):
from music21 import note

s = corpus.parse('bach/bwv66.6.xml')
p1 = s.parts['Soprano']
p1 = s.parts['#Soprano']
for classStr in ['Clef', 'KeySignature', 'TimeSignature']:
self.assertEqual(p1.flatten().getElementsByClass(
classStr)[0].measureNumber, 0)
Expand Down Expand Up @@ -4594,7 +4594,7 @@ def testPickupMeasuresImported(self):
self.maxDiff = None
s = corpus.parse('bach/bwv103.6')

p = s.parts['soprano']
p = s.parts['#soprano']
m1 = p.getElementsByClass(stream.Measure).first()

self.assertEqual([n.offset for n in m1.notesAndRests], [0.0, 0.5])
Expand Down
11 changes: 10 additions & 1 deletion music21/common/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,17 @@ def deprecated(method, startDate=None, removeDate=None, message=None):

@wraps(method)
def func_wrapper(*args, **kwargs):
if len(args) > 1 and args[1] in ('_ipython_canary_method_should_not_exist_',
'_repr_mimebundle_'):
# false positive from IPython for StreamIterator.__getattr__
# can remove after v9.
falsePositive = True
else:

falsePositive = False

# TODO: look at sys.warnstatus.
if callInfo['calledAlready'] is False:
if callInfo['calledAlready'] is False and not falsePositive:
warnings.warn(callInfo['message'],
exceptions21.Music21DeprecationWarning,
stacklevel=2)
Expand Down
6 changes: 3 additions & 3 deletions music21/midi/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3143,7 +3143,7 @@ def testAnacrusisTiming(self):
s = corpus.parse('bach/bwv103.6')

# get just the soprano part
soprano = s.parts['soprano']
soprano = s.parts['#soprano']
mts = streamHierarchyToMidiTracks(soprano)[1] # get one

# first note-on is not delayed, even w anacrusis
Expand All @@ -3169,15 +3169,15 @@ def testAnacrusisTiming(self):
<music21.midi.DeltaTime (empty) track=1, channel=1>,
<music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=62, velocity=90>]'''

alto = s.parts['alto']
alto = s.parts['#alto']
mta = streamHierarchyToMidiTracks(alto)[1]

found = str(mta.events[:8])
self.assertTrue(common.whitespaceEqual(found, match), found)

# try streams to midi tracks
# get just the soprano part
soprano = s.parts['soprano']
soprano = s.parts['#soprano']
mtList = streamHierarchyToMidiTracks(soprano)
self.assertEqual(len(mtList), 2)

Expand Down
2 changes: 1 addition & 1 deletion music21/musicxml/test_m21ToXml.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ def test_instrumentDoesNotCreateForward(self):
in the toSoundingPitch and not having their durations restored afterwards
leading to Instrument objects being split if the duration was complex
'''
alto = corpus.parse('bach/bwv57.8').parts['Alto']
alto = corpus.parse('bach/bwv57.8').parts['#Alto']
alto.measure(7).timeSignature = meter.TimeSignature('6/8')
newAlto = alto.flat.getElementsNotOfClass(meter.TimeSignature).stream()
newAlto.insert(0, meter.TimeSignature('2/4'))
Expand Down
31 changes: 23 additions & 8 deletions music21/prebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
class ProtoM21Object:
'''
A class for pseudo-m21 objects to inherit from. Any object can inherit from
ProtoM21Object and it makes sense for anything a user is likely to encounter
ProtoM21Object, and it makes sense for anything a user is likely to encounter
to inherit from it. Certain translators, etc. can choose to skip it.
>>> class PitchCounter(prebase.ProtoM21Object):
Expand Down Expand Up @@ -168,11 +168,11 @@ def classes(self) -> Tuple[str, ...]:
@property
def classSet(self) -> FrozenSet[Union[str, type]]:
'''
Returns a set (that is, unordered, but indexed) of all of the classes that
Returns a set (that is, unordered, but indexed) of all classes that
this class belongs to, including
string names, fullyQualified string names, and objects themselves.
It's cached on a per class basis, so makes for a really fast way of checking to
It's cached on a per-class basis, so makes for a really fast way of checking to
see if something belongs
to a particular class when you don't know if the user has given a string,
a fully qualified string name, or an object.
Expand All @@ -198,12 +198,23 @@ def classSet(self) -> FrozenSet[Union[str, type]]:
True
>>> sorted([s for s in n.classSet if isinstance(s, str)])
['GeneralNote', 'Music21Object', 'NotRest', 'Note', 'ProtoM21Object',
['GeneralNote',
'Music21Object',
'NotRest',
'Note',
'ProtoM21Object',
'base.Music21Object',
'builtins.object',
'music21.base.Music21Object',
'music21.note.GeneralNote', 'music21.note.NotRest', 'music21.note.Note',
'music21.note.GeneralNote',
'music21.note.NotRest',
'music21.note.Note',
'music21.prebase.ProtoM21Object',
'object']
'note.GeneralNote',
'note.NotRest',
'note.Note',
'object',
'prebase.ProtoM21Object']
>>> sorted([s for s in n.classSet if not isinstance(s, str)], key=lambda x: x.__name__)
[<class 'music21.note.GeneralNote'>,
Expand All @@ -212,14 +223,18 @@ def classSet(self) -> FrozenSet[Union[str, type]]:
<class 'music21.note.Note'>,
<class 'music21.prebase.ProtoM21Object'>,
<class 'object'>]
changed in v8 -- partially qualified objects such as 'note.Note' have been added.
'''
try:
return self._classSetCacheDict[self.__class__]
except KeyError:
classList: List[Union[str, type]] = list(self.classes)
classList.extend(self.__class__.mro())
classList.extend(x.__module__ + '.' + x.__name__ for x in self.__class__.mro())

fullyQualifiedStrings = [x.__module__ + '.' + x.__name__ for x in self.__class__.mro()]
classList.extend(fullyQualifiedStrings)
partiallyQualifiedStrings = [x.replace('music21.', '') for x in fullyQualifiedStrings]
classList.extend(partiallyQualifiedStrings)
classSet = frozenset(classList)
self._classSetCacheDict[self.__class__] = classSet
return classSet
Expand Down
6 changes: 3 additions & 3 deletions music21/scale/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3373,13 +3373,13 @@ def testBasic(self):
sc1 = scale.MajorScale()
# deriving a new scale from the pitches found in a collection
s = corpus.parse('bwv66.6')
sc3 = sc1.derive(s.parts['soprano'])
sc3 = sc1.derive(s.parts['#soprano'])
self.assertEqual(str(sc3), '<music21.scale.MajorScale A major>')

sc3 = sc1.derive(s.parts['tenor'])
sc3 = sc1.derive(s.parts['#tenor'])
self.assertEqual(str(sc3), '<music21.scale.MajorScale A major>')

sc3 = sc2.derive(s.parts['bass'])
sc3 = sc2.derive(s.parts['#bass'])
self.assertEqual(str(sc3), '<music21.scale.MinorScale F# minor>')

# composing with a scale
Expand Down
64 changes: 23 additions & 41 deletions music21/stream/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,7 @@ class StreamDeprecationWarning(UserWarning):


# -----------------------------------------------------------------------------


class Stream(core.StreamCoreMixin, base.Music21Object, Generic[M21ObjType]):
class Stream(core.StreamCore, Generic[M21ObjType]):
'''
This is the fundamental container for Music21Objects;
objects may be ordered and/or placed in time based on
Expand Down Expand Up @@ -281,8 +279,7 @@ def __init__(self,
*args,
# restrictClass: Type[M21ObjType] = base.Music21Object,
**keywords):
base.Music21Object.__init__(self, **keywords)
core.StreamCoreMixin.__init__(self)
super().__init__(self, *args, **keywords)

self.streamStatus = streamStatus.StreamStatus(self)
self._unlinkedDuration = None
Expand Down Expand Up @@ -527,21 +524,15 @@ def __getitem__(self,
>>> c.groups.append('ghost')
>>> e.groups.append('ghost')

'Note' is treated as a class name and returns a `RecursiveIterator`:

>>> for n in s['Note']:
... print(n.name, end=' ')
C C# D E F G A

'.ghost', because it begins with `.`, is treated as a class name and
returns a `RecursiveIterator`:


>>> for n in s['.ghost']:
... print(n.name, end=' ')
C E

A query selector with a `#`:
A query selector with a `#` returns the single element matching that
element or returns None if there is no match:

>>> s['#last_a']
<music21.note.Note A>
Expand All @@ -556,15 +547,21 @@ def __getitem__(self,
Traceback (most recent call last):
TypeError: Streams can get items by int, slice, class, or string query; got <class 'float'>


Changed in v7:

- out of range indexes now raise an IndexError, not StreamException
- strings ('Note', '#id', '.group') are now treated like a query selector.
- strings ('music21.note.Note', '#id', '.group') are now treated like a query selector.
- slices with negative indices now supported
- Unsupported types now raise TypeError
- Class and Group searches now return a recursive `StreamIterator` rather than a `Stream`
- Slice searches now return a list of elements rather than a `Stream`

Changed in v8:
- for strings: only fully-qualified names such as "music21.note.Note" or
partially-qualified names such as "note.Note" are
supported as class names. Better to use a literal type or explicitly call
.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.
'''
# 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 @@ -1585,7 +1582,7 @@ def remove(self,
raise ImmutableStreamException('Cannot remove from an immutable stream')
# TODO: Next to clean up... a doozy -- filter out all the different options.

# TODO: Add a renumber measures option
# TODO: Add an option to renumber measures
# TODO: Shift offsets if recurse is True
if shiftOffsets is True and recurse is True: # pragma: no cover
raise StreamException(
Expand Down Expand Up @@ -4756,7 +4753,7 @@ def measureOffsetMap(self, classFilterList=None):


>>> chorale = corpus.parse('bach/bwv324.xml')
>>> alto = chorale.parts['alto']
>>> alto = chorale.parts['#alto']
>>> altoMeasures = alto.measureOffsetMap()
>>> altoMeasures
OrderedDict([(0.0, [<music21.stream.Measure 1 offset=0.0>]),
Expand Down Expand Up @@ -7880,7 +7877,7 @@ def highestTime(self):
in quarter lengths. This value usually represents the last
"release" in the Stream.

Stream.duration is usually equal to the highestTime
Stream.duration is normally equal to the highestTime
expressed as a Duration object, but it can be set separately
for advanced operations.

Expand Down Expand Up @@ -13759,6 +13756,7 @@ def makeNotation(self,
If `inPlace` is True, this is done in-place;
if `inPlace` is False, this returns a modified deep copy.
'''
returnStream: Score
if inPlace:
returnStream = self
else:
Expand All @@ -13784,8 +13782,6 @@ def makeNotation(self,
# no matter, let's just be extra cautious and run this here (Feb 2021 - JTW)
returnStream.coreElementsChanged()
else: # call the base method
if TYPE_CHECKING:
assert isinstance(returnStream, Score)
super(Score, returnStream).makeNotation(meterStream=meterStream,
refStreamOrTimeRange=refStreamOrTimeRange,
inPlace=True,
Expand Down Expand Up @@ -13999,7 +13995,7 @@ def show(self, fmt=None, app=None, **keywords):
class SpannerStorage(Stream):
'''
For advanced use. This Stream subclass is only used
inside of a Spanner object to provide object storage
inside a Spanner object to provide object storage
of connected elements (things the Spanner spans).

This subclass name can be used to search in an
Expand Down Expand Up @@ -14059,32 +14055,18 @@ def replace(self,
class VariantStorage(Stream):
'''
For advanced use. This Stream subclass is only
used inside of a Variant object to provide object
used inside a Variant object to provide object
storage of connected elements (things the Variant
defines).

This subclass name can be used to search in an
object's .sites and find any and all
locations that are VariantStorage objects.
locations that are VariantStorage objects. It also
ensures that they are pickled properly as Streams on a non-Stream
object.

A `variantParent` keyword argument must be provided
by the Variant in creation.

# TODO v7: rename variantParent to client
Changed in v.8 -- variantParent is removed. Never used.
'''

def __init__(self, *arguments, **keywords):
super().__init__(*arguments, **keywords)
# must provide a keyword argument with a reference to the variant
# parent
self.variantParent = None
if 'variantParent' in keywords:
self.variantParent = keywords['variantParent']


# -----------------------------------------------------------------------------


# -----------------------------------------------------------------------------


Expand Down
Loading

0 comments on commit 137bf09

Please sign in to comment.