diff --git a/.github/workflows/pythonpylint.yml b/.github/workflows/pythonpylint.yml index 151e417a27..a7eb4fae8a 100644 --- a/.github/workflows/pythonpylint.yml +++ b/.github/workflows/pythonpylint.yml @@ -64,4 +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/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 diff --git a/music21/base.py b/music21/base.py index 6aa67ed361..5475f4ffa0 100644 --- a/music21/base.py +++ b/music21/base.py @@ -832,7 +832,9 @@ def clearCache(self, **keywords): New in v.6 -- exposes previously hidden functionality. ''' - self._cache = {} + # do not replace with self._cache.clear() -- leaves terrible + # state for shallow copies. + self._cache: Dict[str, Any] = {} @overload def getOffsetBySite( diff --git a/music21/common/types.py b/music21/common/types.py index 3df6210bfd..dd0686c4d4 100644 --- a/music21/common/types.py +++ b/music21/common/types.py @@ -21,6 +21,9 @@ OffsetQLIn = Union[int, float, Fraction] StreamType = TypeVar('StreamType', bound='music21.stream.Stream') +StreamType2 = TypeVar('StreamType2', bound='music21.stream.Stream') M21ObjType = TypeVar('M21ObjType', bound='music21.base.Music21Object') +M21ObjType2 = TypeVar('M21ObjType2', bound='music21.base.Music21Object') # when you need another + ClassListType = Union[str, Iterable[str], Type[M21ObjType], Iterable[Type[M21ObjType]]] StepName = Literal['C', 'D', 'E', 'F', 'G', 'A', 'B'] diff --git a/music21/serial.py b/music21/serial.py index 7276fa69b9..19dd109e96 100644 --- a/music21/serial.py +++ b/music21/serial.py @@ -704,8 +704,6 @@ def matrix(self): ... >>> [str(e.pitch) for e in s37[0]] ['C', 'B', 'G', 'G#', 'E-', 'C#', 'D', 'B-', 'F#', 'F', 'E', 'A'] - - ''' # note: do not want to return a TwelveToneRow() type, as this will # add again the same pitches to the elements list twice. @@ -718,8 +716,8 @@ def matrix(self): i = 0 for row in matrix: i += 1 - rowObject = copy.copy(self) - rowObject.elements = [] + rowObject = self.__class__() + rowObject.mergeAttributes(self) rowObject.id = 'row-' + str(i) for p in row: # iterate over pitch class values n = note.Note() @@ -1115,6 +1113,14 @@ def __init__(self, composer=None, opus=None, title=None, row=None): self.opus = opus self.title = title + def mergeAttributes(self, other): + super().mergeAttributes(other) + if not isinstance(other, HistoricalTwelveToneRow): + return + self.composer = other.composer + self.opus = other.opus + self.title = other.title + def _reprInternal(self): return f'{self.composer} {self.opus} {self.title}' diff --git a/music21/stream/base.py b/music21/stream/base.py index 6ef9429d25..11f0600c8a 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -34,7 +34,8 @@ from fractions import Fraction from math import isclose from typing import (Dict, Iterable, List, Optional, Set, Tuple, cast, - TypeVar, Type, Union, Generic, Literal, overload) + TypeVar, Type, Union, Generic, Literal, overload, + Sequence, TYPE_CHECKING) from music21 import base @@ -67,7 +68,7 @@ from music21.common.numberTools import opFrac from music21.common.enums import GatherSpanners, OffsetSpecial -from music21.common.types import StreamType, M21ObjType, OffsetQL +from music21.common.types import StreamType, M21ObjType, OffsetQL, OffsetQLSpecial from music21 import environment @@ -269,15 +270,16 @@ class Stream(core.StreamCoreMixin, base.Music21Object, Generic[M21ObjType]): musicxml tag) and only if this is the outermost Stream being shown. ''', - 'restrictClass': ''' - All elements in the stream are required to be of this class - or a subclass of that class. Currently not enforced. Used - for type-checking. - ''', + # 'restrictClass': ''' + # All elements in the stream are required to be of this class + # or a subclass of that class. Currently not enforced. Used + # for type-checking. + # ''', } - - def __init__(self, givenElements=None, *args, - restrictClass: Type[M21ObjType] = base.Music21Object, + def __init__(self, + givenElements=None, + *args, + # restrictClass: Type[M21ObjType] = base.Music21Object, **keywords): base.Music21Object.__init__(self, **keywords) core.StreamCoreMixin.__init__(self) @@ -287,8 +289,8 @@ def __init__(self, givenElements=None, *args, self.autoSort = True - # all elements in the stream need to be of this class. - self.restrictClass = restrictClass + # # all elements in the stream need to be of this class. + # self.restrictClass = restrictClass # these should become part of style or something else... self.definesExplicitSystemBreaks = False @@ -333,8 +335,10 @@ def _reprInternal(self) -> str: if self.id is not None: if self.id != id(self) and str(self.id) != str(id(self)): return str(self.id) - else: + elif isinstance(self.id, int): return hex(self.id) + else: # pragma: no cover + return '' else: # pragma: no cover return '' @@ -390,7 +394,8 @@ def __iter__(self) -> iterator.StreamIterator[M21ObjType]: specialized :class:`music21.stream.StreamIterator` class, which adds necessary Stream-specific features. ''' - return iterator.StreamIterator[M21ObjType](self) + return cast(iterator.StreamIterator[M21ObjType], + iterator.StreamIterator(self)) def iter(self) -> iterator.StreamIterator[M21ObjType]: ''' @@ -423,7 +428,7 @@ def __getitem__( self, k: Type[ChangedM21ObjType] ) -> iterator.RecursiveIterator[ChangedM21ObjType]: - x: iterator.RecursiveIterator[ChangedM21ObjType] = self.recurse() + x = cast(iterator.RecursiveIterator[ChangedM21ObjType], self.recurse()) return x # dummy code def __getitem__(self, @@ -582,17 +587,17 @@ def __getitem__(self, ) # setting active site as cautionary measure self.coreSelfActiveSite(match) - return match + return cast(M21ObjType, match) elif isinstance(k, slice): # get a slice of index values # manually inserting elements is critical to setting the element # locations - searchElements = self._elements + searchElements: List[base.Music21Object] = self._elements if (k.start is not None and k.start < 0) or (k.stop is not None and k.stop < 0): # Must use .elements property to incorporate end elements - searchElements = self.elements + searchElements = list(self.elements) - return searchElements[k] + return cast(M21ObjType, searchElements[k]) elif isinstance(k, type) and issubclass(k, base.Music21Object): return self.recurse().getElementsByClass(k) @@ -694,7 +699,7 @@ def __contains__(self, el): return False @property - def elements(self) -> Tuple[base.Music21Object]: + def elements(self) -> Tuple[M21ObjType, ...]: ''' .elements is a Tuple representing the elements contained in the Stream. @@ -743,11 +748,11 @@ def elements(self) -> Tuple[base.Music21Object]: # coreElementsChanged has been called if not self.isSorted and self.autoSort: self.sort() # will set isSorted to True - self._cache['elements'] = self._elements + self._endElements + self._cache['elements'] = cast(List[M21ObjType], self._elements + self._endElements) return tuple(self._cache['elements']) @elements.setter - def elements(self, value: Union['Stream', Iterable[base.Music21Object]]): + def elements(self, value: Union[Stream, Iterable[base.Music21Object]]): ''' Sets this stream's elements to the elements in another stream (just give the stream, not the stream's .elements), or to a list of elements. @@ -767,17 +772,15 @@ def elements(self, value: Union['Stream', Iterable[base.Music21Object]]): are we going to get the new stream's elements' offsets from? why from their active sites! So don't do this! ''' - if (not common.isListLike(value) - and hasattr(value, 'isStream') - and value.isStream): + self._offsetDict: Dict[int, Tuple[OffsetQLSpecial, base.Music21Object]] = {} + if isinstance(value, Stream): # set from a Stream. Best way to do it - self._offsetDict = {} - self._elements = list(value._elements) # copy list. + self._elements: List[base.Music21Object] = list(value._elements) # copy list. for e in self._elements: self.coreSetElementOffset(e, value.elementOffset(e), addElement=True) e.sites.add(self) self.coreSelfActiveSite(e) - self._endElements = list(value._endElements) + self._endElements: List[base.Music21Object] = list(value._endElements) for e in self._endElements: self.coreSetElementOffset(e, value.elementOffset(e, returnSpecial=True), @@ -788,7 +791,6 @@ def elements(self, value: Union['Stream', Iterable[base.Music21Object]]): # replace the complete elements list self._elements = list(value) self._endElements = [] - self._offsetDict = {} for e in self._elements: self.coreSetElementOffset(e, e.offset, addElement=True) e.sites.add(self) @@ -851,7 +853,7 @@ def __delitem__(self, k): del self._elements[k] self.coreElementsChanged() - def __add__(self: T, other: 'Stream') -> T: + def __add__(self: StreamType, other: 'Stream') -> StreamType: ''' Add, or concatenate, two Streams. @@ -1187,11 +1189,11 @@ def staffLines(self, newStaffLines: int): .getElementsByOffset(0.0) .getElementsByClass(layout.StaffLayout) ) - if not staffLayouts: + firstLayout = staffLayouts.first() + if not firstLayout: sl: layout.StaffLayout = layout.StaffLayout(staffLines=newStaffLines) self.insert(0.0, sl) else: - firstLayout = staffLayouts.first() firstLayout.staffLines = newStaffLines def clear(self) -> None: @@ -1212,7 +1214,7 @@ def clear(self) -> None: >>> m.number 3 ''' - self.elements = [] + self.elements = () def cloneEmpty(self: StreamType, derivationMethod: Optional[str] = None) -> StreamType: ''' @@ -1243,7 +1245,7 @@ def cloneEmpty(self: StreamType, derivationMethod: Optional[str] = None) -> Stre returnObj.mergeAttributes(self) # get groups, optional id return returnObj - def mergeAttributes(self, other: 'Stream'): + def mergeAttributes(self, other): ''' Merge relevant attributes from the Other stream into this one. @@ -1261,6 +1263,8 @@ def mergeAttributes(self, other: 'Stream'): 0 ''' super().mergeAttributes(other) + if not isinstance(other, Stream): + return for attr in ('autoSort', 'isSorted', 'definesExplicitSystemBreaks', 'definesExplicitPageBreaks', '_atSoundingPitch', '_mutable'): @@ -1391,7 +1395,7 @@ def mergeElements(self, other, classFilterList=None): # self.storeAtEnd(e) self.coreElementsChanged() - def index(self, el: M21ObjType) -> int: + def index(self, el: base.Music21Object) -> int: ''' Return the first matched index for the specified object. @@ -1445,7 +1449,7 @@ def index(self, el: M21ObjType) -> int: raise StreamException(f'cannot find object ({el}) in Stream') def remove(self, - targetOrList: Union[base.Music21Object, List[base.Music21Object]], + targetOrList: Union[base.Music21Object, Sequence[base.Music21Object]], *, shiftOffsets=False, recurse=False): @@ -1587,16 +1591,23 @@ def remove(self, raise StreamException( 'Cannot do both shiftOffsets and recurse search at the same time...yet') + targetList: List[base.Music21Object] if not common.isListLike(targetOrList): + if TYPE_CHECKING: + assert isinstance(targetOrList, base.Music21Object) targetList = [targetOrList] - elif len(targetOrList) > 1: + elif isinstance(targetOrList, Sequence) and len(targetOrList) > 1: + if TYPE_CHECKING: + assert not isinstance(targetOrList, base.Music21Object) try: - targetList = sorted(targetOrList, key=self.elementOffset) + targetList = list(sorted(targetOrList, key=self.elementOffset)) except sites.SitesException: # will not be found if recursing, it's not such a big deal... - targetList = targetOrList + targetList = list(targetOrList) else: - targetList = targetOrList + if TYPE_CHECKING: + assert not isinstance(targetOrList, base.Music21Object) + targetList = list(targetOrList) shiftDur = 0.0 # for shiftOffsets @@ -1894,7 +1905,7 @@ def _replaceSpannerBundleForDeepcopy(self, new): def setElementOffset( self, element: base.Music21Object, - offset: Union[int, float, Fraction, str], + offset: Union[int, float, Fraction, OffsetSpecial], ): ''' Sets the Offset for an element that is already in a given stream. @@ -2712,7 +2723,9 @@ def insertAndShift(self, offsetOrItemOrList, itemOrNone=None): # -------------------------------------------------------------------------- # searching and replacing routines - def setDerivationMethod(self, derivationMethod, recurse=False) -> None: + def setDerivationMethod(self, + derivationMethod: str, + recurse=False) -> None: ''' Sets the .derivation.method for each element in the Stream if it has a .derivation object. @@ -2734,10 +2747,10 @@ def setDerivationMethod(self, derivationMethod, recurse=False) -> None: >>> s2.recurse().notes[-1].derivation from via '__deepcopy__'> ''' - if recurse: - sIter = self.recurse() - else: + if not recurse: sIter = self.iter() + else: + sIter = self.recurse() for el in sIter: if el.derivation is not None: @@ -2991,7 +3004,8 @@ def processContainer(container: Stream): if isinstance(complexObj, note.Rest) and complexObj.fullMeasure in (True, 'always'): continue if isinstance(complexObj, note.Rest) and complexObj.fullMeasure == 'auto': - if container.isMeasure and (complexObj.duration == container.barDuration): + if (isinstance(container, Measure) + and (complexObj.duration == container.barDuration)): continue elif ('Voice' in container.classes and container.activeSite diff --git a/music21/stream/core.py b/music21/stream/core.py index bd5db76cce..79d34dfa63 100644 --- a/music21/stream/core.py +++ b/music21/stream/core.py @@ -21,6 +21,8 @@ All functions here will eventually begin with `.core`. ''' +from __future__ import annotations + import copy from typing import List, Dict, Union, Tuple, Optional, TYPE_CHECKING from fractions import Fraction @@ -29,12 +31,15 @@ from music21.base import Music21Object from music21.common.enums import OffsetSpecial from music21.common.numberTools import opFrac -from music21.common.types import StreamType +from music21.common.types import OffsetQLSpecial from music21 import spanner from music21 import tree from music21.exceptions21 import StreamException, ImmutableStreamException from music21.stream.iterator import StreamIterator, RecursiveIterator +if TYPE_CHECKING: + from typing import Any + # pylint: disable=attribute-defined-outside-init class StreamCoreMixin: @@ -46,7 +51,7 @@ def __init__(self): # the _offsetDict is a dictionary where id(element) is the # index and the value is a tuple of offset and element. # offsets can be floats, Fractions, or a member of the enum OffsetSpecial - self._offsetDict: Dict[int, Tuple[Union[float, Fraction, str], Music21Object]] = {} + self._offsetDict: Dict[int, Tuple[OffsetQLSpecial, Music21Object]] = {} # self._elements stores Music21Object objects. self._elements: List[Music21Object] = [] @@ -159,7 +164,7 @@ def coreAppend( def coreSetElementOffset( self, element: Music21Object, - offset: Union[int, float, Fraction, str], + offset: Union[int, float, Fraction, OffsetSpecial], *, addElement=False, setActiveSite=True @@ -287,15 +292,14 @@ def coreElementsChanged( indexCache = self._cache['index'] # always clear cache when elements have changed # for instance, Duration will change. - # noinspection PyAttributeOutsideInit - self._cache = {} # cannot call clearCache() because defined on Stream via Music21Object + self.clearCache() if keepIndex and indexCache is not None: self._cache['index'] = indexCache - def coreCopyAsDerivation(self: StreamType, + def coreCopyAsDerivation(self, methodName: str, *, recurse=True, - deep=True) -> StreamType: + deep=True) -> 'music21.stream.Stream[Any]': ''' Make a copy of this stream with the proper derivation set. @@ -310,13 +314,21 @@ def coreCopyAsDerivation(self: StreamType, >>> s2[0].derivation.method 'exampleCopy' ''' + if TYPE_CHECKING: + from music21 import stream + assert isinstance(self, stream.Stream) if deep: post = copy.deepcopy(self) else: # pragma: no cover post = copy.copy(self) - post.derivation.method = methodName # type: ignore + + if TYPE_CHECKING: + from music21 import stream + assert isinstance(post, stream.Stream) + + post.derivation.method = methodName if recurse and deep: - post.setDerivationMethod(methodName, recurse=True) # type: ignore + post.setDerivationMethod(methodName, recurse=True) return post def coreHasElementByMemoryLocation(self, objId: int) -> bool: diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py index 79ac43a58d..df4f7af3c6 100644 --- a/music21/stream/iterator.py +++ b/music21/stream/iterator.py @@ -1788,6 +1788,7 @@ def __next__(self) -> M21ObjType: ) newStartOffset = (self.iteratorStartOffsetInHierarchy + self.srcStream.elementOffset(e)) + self.childRecursiveIterator.iteratorStartOffsetInHierarchy = newStartOffset if self.matchesFilters(e) is False: continue @@ -2002,7 +2003,7 @@ def getElementsByClass(self, classFilterList: Type[ChangedM21ObjType], *, returnClone: bool = True) -> RecursiveIterator[ChangedM21ObjType]: - x: RecursiveIterator[ChangedM21ObjType] = self.__class__(self.streamObj) + x = cast(RecursiveIterator[ChangedM21ObjType], self.__class__(self.streamObj)) return x # dummy code @overload @@ -2014,7 +2015,7 @@ def getElementsByClass(self, return x # dummy code - def getElementsByClass(self: _SIter, + def getElementsByClass(self, classFilterList: Union[ str, Type[ChangedM21ObjType], @@ -2029,7 +2030,7 @@ def getElementsByClass(self: _SIter, if isinstance(classFilterList, type) and issubclass(classFilterList, base.Music21Object): return cast(RecursiveIterator[ChangedM21ObjType], out) else: - return cast(RecursiveIterator[base.Music21Object], out) + return cast(RecursiveIterator[M21ObjType], out) class Test(unittest.TestCase):