Repository: cuthbertLab/music21
Pull request #1423 (2022-09-15 23:09:57)

Add typing for contextSites, yieldSites
Add proper typing for contextSites and yieldSites
Make RecurseType a Generator

differentiate between ContextTuple and ContextSortTuple

Rename newly introduced in v8 'appendOrInsert' in stream keyword to 'givenElementsBehavior' change to an enum

Find slower tests and move them up in multiprocessTest


Patch:
diff --git a/music21/base.py b/music21/base.py
index eab30568a..d133a7463 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -43,7 +43,6 @@ import copy
 import warnings
 import weakref
 
-from collections import namedtuple
 from importlib.util import find_spec
 
 # for type annotation only
@@ -128,7 +127,15 @@ class ElementException(exceptions21.Music21Exception):
 
 # -----------------------------------------------------------------------------
 # for contextSites searches...
-ContextTuple = namedtuple('ContextTuple', ['site', 'offset', 'recurseType'])
+class ContextTuple(t.NamedTuple):
+    site: 'music21.stream.Stream'
+    offset: OffsetQL
+    recurseType: 'music21.stream.enums.RecursionType'
+
+class ContextSortTuple(t.NamedTuple):
+    site: 'music21.stream.Stream'
+    offset: SortTuple
+    recurseType: 'music21.stream.enums.RecursionType'
 
 
 # pseudo class for returning splitAtX() type commands.
@@ -1706,35 +1713,67 @@ class Music21Object(prebase.ProtoM21Object):
         # nothing found...
         return None
 
+
+    @overload
     def contextSites(
         self,
         *,
+        returnSortTuples: t.Literal[True],
         callerFirst=None,
         memo=None,
-        offsetAppend=0.0,
-        sortByCreationTime: t.Union[str, bool] = False,
+        offsetAppend: OffsetQL = 0.0,
+        sortByCreationTime: t.Union[t.Literal['reverse'], bool] = False,
         priorityTarget=None,
-        returnSortTuples=False,
         followDerivation=True,
         priorityTargetOnly=False,
-    ):
+    ) -> t.Generator[ContextSortTuple, None, None]:
+        pass
+
+    @overload
+    def contextSites(
+        self,
+        *,
+        callerFirst=None,
+        memo=None,
+        offsetAppend: OffsetQL = 0.0,
+        sortByCreationTime: t.Union[t.Literal['reverse'], bool] = False,
+        priorityTarget=None,
+        returnSortTuples: t.Literal[False] = False,
+        followDerivation=True,
+        priorityTargetOnly=False,
+    ) -> t.Generator[ContextTuple, None, None]:
+        pass
+
+
+    def contextSites(
+        self,
+        *,
+        callerFirst=None,
+        memo=None,
+        offsetAppend: OffsetQL = 0.0,
+        sortByCreationTime: t.Union[t.Literal['reverse'], bool] = False,
+        priorityTarget=None,
+        returnSortTuples: bool = False,
+        followDerivation=True,
+        priorityTargetOnly=False,
+    ) -> t.Generator[t.Union[ContextTuple, ContextSortTuple], None, None]:
         '''
         A generator that returns a list of namedtuples of sites to search for a context...
 
         Each tuple contains three elements:
 
-        .site --  Stream object,
+        .site --  Stream object
         .offset -- the offset or position (sortTuple) of this element in that Stream
-        .recurseType -- the method of searching that should be applied to search for a context.
+        .recurseType -- the way of searching that should be applied to search for a context.
 
-        The recurseType methods are:
+        The recurseType values are all music21.stream.enums.RecurseType:
 
-            * 'flatten' -- flatten the stream and then look from this offset backwards.
+            * FLATTEN -- flatten the stream and then look from this offset backwards.
 
-            * 'elementsOnly' -- only search the stream's personal
+            * ELEMENTS_ONLY -- only search the stream's personal
                elements from this offset backwards
 
-            * 'elementsFirst' -- search this stream backwards,
+            * ELEMENTS_FIRST -- search this stream backwards,
                and then flatten and search backwards
 
         >>> c = corpus.parse('bwv66.6')
@@ -1749,9 +1788,9 @@ class Music21Object(prebase.ProtoM21Object):
         >>> for csTuple in n.contextSites(returnSortTuples=True):
         ...      yClearer = (csTuple.site, csTuple.offset.shortRepr(), csTuple.recurseType)
         ...      print(yClearer)
-        (<music21.stream.Measure 3 offset=9.0>, '0.5 <0.20...>', 'elementsFirst')
-        (<music21.stream.Part Alto>, '9.5 <0.20...>', 'flatten')
-        (<music21.stream.Score bach>, '9.5 <0.20...>', 'elementsOnly')
+        (<music21.stream.Measure 3 offset=9.0>, '0.5 <0.20...>', <RecursionType.ELEMENTS_FIRST>)
+        (<music21.stream.Part Alto>, '9.5 <0.20...>', <RecursionType.FLATTEN>)
+        (<music21.stream.Score bach>, '9.5 <0.20...>', <RecursionType.ELEMENTS_ONLY>)
 
         Streams have themselves as the first element in their context sites, at position
         zero and classSortOrder negative infinity.
@@ -1764,12 +1803,28 @@ class Music21Object(prebase.ProtoM21Object):
         >>> m = c[2][4]
         >>> m
         <music21.stream.Measure 3 offset=9.0>
+
+        If returnSortTuples is true then ContextSortTuples are returned, where the
+        second element is a SortTuple:
+
+        >>> for csTuple in m.contextSites(returnSortTuples=True):
+        ...     print(csTuple)
+        ContextSortTuple(site=<music21.stream.Measure 3 offset=9.0>,
+                         offset=SortTuple(atEnd=0, offset=0.0, priority=-inf, ...),
+                         recurseType=<RecursionType.ELEMENTS_FIRST>)
+        ContextSortTuple(...)
+        ContextSortTuple(...)
+
+        Because SortTuples are so detailed, we'll use their `shortRepr()` to see the
+        values, removing the insertIndex because it changes from run to run:
+
         >>> for csTuple in m.contextSites(returnSortTuples=True):
         ...      yClearer = (csTuple.site, csTuple.offset.shortRepr(), csTuple.recurseType)
         ...      print(yClearer)
-        (<music21.stream.Measure 3 offset=9.0>, '0.0 <-inf.-20...>', 'elementsFirst')
-        (<music21.stream.Part Alto>, '9.0 <0.-20...>', 'flatten')
-        (<music21.stream.Score bach>, '9.0 <0.-20...>', 'elementsOnly')
+        (<music21.stream.Measure 3 offset=9.0>, '0.0 <-inf.-20...>', <RecursionType.ELEMENTS_FIRST>)
+        (<music21.stream.Part Alto>, '9.0 <0.-20...>', <RecursionType.FLATTEN>)
+        (<music21.stream.Score bach>, '9.0 <0.-20...>', <RecursionType.ELEMENTS_ONLY>)
+
 
         Here we make a copy of the earlier measure, and we see that its contextSites
         follow the derivationChain from the original measure and still find the Part
@@ -1783,13 +1838,13 @@ class Music21Object(prebase.ProtoM21Object):
         ...      print(csTuple, mCopy in csTuple.site)
         ContextTuple(site=<music21.stream.Measure 3333 offset=0.0>,
                      offset=0.0,
-                     recurseType='elementsFirst') False
+                     recurseType=<RecursionType.ELEMENTS_FIRST>) False
         ContextTuple(site=<music21.stream.Part Alto>,
                      offset=9.0,
-                     recurseType='flatten') False
+                     recurseType=<RecursionType.FLATTEN>) False
         ContextTuple(site=<music21.stream.Score bach>,
                      offset=9.0,
-                     recurseType='elementsOnly') False
+                     recurseType=<RecursionType.ELEMENTS_ONLY>) False
 
         If followDerivation were False, then the Part and Score would not be found.
 
@@ -1797,7 +1852,7 @@ class Music21Object(prebase.ProtoM21Object):
         ...     print(csTuple)
         ContextTuple(site=<music21.stream.Measure 3333 offset=0.0>,
                      offset=0.0,
-                     recurseType='elementsFirst')
+                     recurseType=<RecursionType.ELEMENTS_FIRST>)
 
         >>> partIterator = c.parts
         >>> m3 = partIterator[1].measure(3)
@@ -1805,13 +1860,13 @@ class Music21Object(prebase.ProtoM21Object):
         ...      print(csTuple)
         ContextTuple(site=<music21.stream.Measure 3 offset=9.0>,
                      offset=0.0,
-                     recurseType='elementsFirst')
+                     recurseType=<RecursionType.ELEMENTS_FIRST>)
         ContextTuple(site=<music21.stream.Part Alto>,
                      offset=9.0,
-                     recurseType='flatten')
+                     recurseType=<RecursionType.FLATTEN>)
         ContextTuple(site=<music21.stream.Score bach>,
                      offset=9.0,
-                     recurseType='elementsOnly')
+                     recurseType=<RecursionType.ELEMENTS_ONLY>)
 
         Sorting order:
 
@@ -1891,6 +1946,7 @@ class Music21Object(prebase.ProtoM21Object):
         *  changed in v5.5: all arguments are keyword only.
         *  changed in v6: added `priorityTargetOnly=False` to only search in the
             context of the priorityTarget.
+        *  changed in v8: returnSortTuple=True returns a new ContextSortTuple
         '''
         from music21 import stream
 
@@ -1909,7 +1965,7 @@ class Music21Object(prebase.ProtoM21Object):
                         offset=0.0,
                         priority=float('-inf')
                     )
-                    yield ContextTuple(streamSelf, selfSortTuple, recursionType)
+                    yield ContextSortTuple(streamSelf, selfSortTuple, recursionType)
                 else:
                     yield ContextTuple(streamSelf, 0.0, recursionType)
                 memo.append(streamSelf)
@@ -1945,7 +2001,7 @@ class Music21Object(prebase.ProtoM21Object):
 
             recursionType = siteObj.recursionType
             if returnSortTuples:
-                yield ContextTuple(siteObj, positionInStream, recursionType)
+                yield ContextSortTuple(siteObj, positionInStream, recursionType)
             else:
                 yield ContextTuple(siteObj, positionInStream.offset, recursionType)
 
@@ -1974,7 +2030,7 @@ class Music21Object(prebase.ProtoM21Object):
                     #                                                inStreamPos.shortRepr(),
                     #                                                recurType))
                     if returnSortTuples:
-                        yield ContextTuple(topLevel, hypotheticalPosition, recurType)
+                        yield ContextSortTuple(topLevel, hypotheticalPosition, recurType)
                     else:
                         yield ContextTuple(topLevel, inStreamOffset, recurType)
                     memo.append(topLevel)
@@ -1999,7 +2055,7 @@ class Music21Object(prebase.ProtoM21Object):
                     environLocal.printDebug(
                         f'Yielding {derivedCsTuple} from derivedObject contextSites'
                     )
-                    offsetAdjustedCsTuple = ContextTuple(
+                    offsetAdjustedCsTuple = ContextSortTuple(
                         derivedCsTuple.site,
                         derivedCsTuple.offset.modify(offset=derivedCsTuple[1].offset
                                                             + offsetAppend),
@@ -2469,7 +2525,14 @@ class Music21Object(prebase.ProtoM21Object):
         else:
             self._naiveOffset = offset
 
-    def sortTuple(self, useSite=False, raiseExceptionOnMiss=False):
+    def sortTuple(self,
+                  useSite: t.Union[
+                      t.Literal[False],
+                      None,
+                      'music21.stream.Stream',
+                  ] = False,
+                  raiseExceptionOnMiss: bool = False
+                  ) -> SortTuple:
         '''
         Returns a collections.namedtuple called SortTuple(atEnd, offset, priority, classSortOrder,
         isNotGrace, insertIndex)
@@ -2499,6 +2562,8 @@ class Music21Object(prebase.ProtoM21Object):
         6) The last tie-breaker is the creation time (insertIndex) of the site object
         represented by the activeSite.
 
+        By default, the site used will be the activeSite:
+
         >>> n = note.Note()
         >>> n.offset = 4.0
         >>> n.priority = -3
@@ -2571,14 +2636,18 @@ class Music21Object(prebase.ProtoM21Object):
         music21.sites.SitesException: an entry for this object 0x... is not stored in
             stream <music21.stream.Stream aloneStream>
         '''
+        useSiteNoFalse: t.Optional['music21.stream.Stream']
         if useSite is False:  # False or a Site; since None is a valid site, default is False
-            useSite = self.activeSite
+            useSiteNoFalse = self.activeSite
+        else:
+            useSiteNoFalse = useSite
 
-        if useSite is None:
+        foundOffset: t.Union[OffsetQL, OffsetSpecial]
+        if useSiteNoFalse is None:
             foundOffset = self.offset
         else:
             try:
-                foundOffset = useSite.elementOffset(self, returnSpecial=True)
+                foundOffset = useSiteNoFalse.elementOffset(self, returnSpecial=True)
             except SitesException:
                 if raiseExceptionOnMiss:
                     raise
@@ -2586,11 +2655,12 @@ class Music21Object(prebase.ProtoM21Object):
                     # activeSite may have vanished! or does not have the element
                 foundOffset = self._naiveOffset
 
+        offset: OffsetQL
         if foundOffset == OffsetSpecial.AT_END:
             offset = 0.0
             atEnd = 1
-        else:
-            offset = foundOffset
+        else:  # no other OffsetSpecials currently exist.
+            offset = t.cast(OffsetQL, foundOffset)
             atEnd = 0
 
         if self.duration.isGrace:
@@ -2598,12 +2668,11 @@ class Music21Object(prebase.ProtoM21Object):
         else:
             isNotGrace = 1
 
-        if (useSite is not False
-                and self.sites.hasSiteId(id(useSite))):
+        if self.sites.hasSiteId(id(useSite)):
             insertIndex = self.sites.siteDict[id(useSite)].globalSiteIndex
         elif self.activeSite is not None:
             insertIndex = self.sites.siteDict[id(self.activeSite)].globalSiteIndex
-        else:
+        else:  # for None, use this instead of default of -2.
             insertIndex = 0
 
         return SortTuple(atEnd, offset, self.priority,
@@ -3415,7 +3484,8 @@ class Music21Object(prebase.ProtoM21Object):
             for cs in self.contextSites():
                 m = cs[0]
                 if m.isMeasure:
-                    mNumber = m.number
+                    # mypy does not know that isMeasure is a typeGuard.
+                    mNumber = m.number  # type: ignore
         return mNumber
 
     def _getMeasureOffset(self, includeMeasurePadding=True) -> t.Union[float, fractions.Fraction]:
diff --git a/music21/sites.py b/music21/sites.py
index 09b6ce3f4..f41667ce4 100644
--- a/music21/sites.py
+++ b/music21/sites.py
@@ -16,6 +16,7 @@ import collections
 import unittest
 import weakref
 import typing as t
+from typing import overload  # for some reason does not work in PyCharm if not directly imported
 
 from music21 import common
 from music21 import exceptions21
@@ -379,10 +380,31 @@ class Sites(common.SlottedObjectMixin):
         self.siteDict = collections.OrderedDict([(None, _NoneSiteRef), ])
         self._lastID = -1  # cannot be None
 
+    @overload
     def yieldSites(self,
-                   sortByCreationTime: t.Union[str, bool] = False,
+                   *,
+                   excludeNone: t.Literal[True],
+                   sortByCreationTime: t.Union[bool, t.Literal['reverse']] = False,
                    priorityTarget=None,
-                   excludeNone=False):
+                   ) -> t.Generator['music21.stream.Stream', None, None]:
+        from music21 import stream
+        yield stream.Stream()
+
+    @overload
+    def yieldSites(self,
+                   *,
+                   excludeNone: bool = False,
+                   sortByCreationTime: t.Union[bool, t.Literal['reverse']] = False,
+                   priorityTarget=None,
+                   ) -> t.Generator[t.Union['music21.stream.Stream', None], None, None]:
+        yield None
+
+    def yieldSites(self,
+                   *,
+                   excludeNone: bool = False,
+                   sortByCreationTime: t.Union[bool, t.Literal['reverse']] = False,
+                   priorityTarget=None,
+                   ) -> t.Generator[t.Union['music21.stream.Stream', None], None, None]:
         # noinspection PyDunderSlots
         '''
         Yield references; order, based on dictionary keys, is from least
@@ -409,13 +431,11 @@ class Sites(common.SlottedObjectMixin):
         >>> aSites.add(aObj)
         >>> aSites.add(bObj)
 
-        Returns a generator (The ellipsis in the repr here is
-        because Python 3.5+ gives a fully qualified name to a generator object):
-
+        Returns a generator:
 
         >>> ys = aSites.yieldSites()
         >>> ys
-        <generator object ...yieldSites at 0x1058085e8>
+        <generator object Sites.yieldSites at 0x1058085e8>
 
         That's no help, so iterate over it instead:
 
@@ -434,11 +454,10 @@ class Sites(common.SlottedObjectMixin):
         b
         a
 
-        *Changes:*
-
-        # v.3: changed dramatically from previously unused version
-        # `sortByCreationTime='reverse'` is removed, since the ordered dict takes
-        care of it and was not working
+        Changed in v.3: changed dramatically from previously unused version
+            `sortByCreationTime='reverse'` is removed, since the ordered dict takes
+            care of it and was not working.
+        Changed in v.8: arguments are keyword only
         '''
         keyRepository = list(self.siteDict.keys())
         if sortByCreationTime is True:
@@ -509,7 +528,9 @@ class Sites(common.SlottedObjectMixin):
 
         * Changed in v5.5: keyword only.
         '''
-        post = list(self.yieldSites(sortByCreationTime, priorityTarget, excludeNone))
+        post = list(self.yieldSites(sortByCreationTime=sortByCreationTime,
+                                    priorityTarget=priorityTarget,
+                                    excludeNone=excludeNone))
 
         # we do this resorting again, because the priority target might not match id, and we
         # want to be extra safe.  If you want fast, use .yieldSites
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 9eea04fd8..b50cbcb82 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -65,6 +65,7 @@ from music21.stream import makeNotation
 from music21.stream import streamStatus
 from music21.stream import iterator
 from music21.stream import filters
+from music21.stream.enums import GivenElementsBehavior, RecursionType
 
 from music21.common.numberTools import opFrac
 from music21.common.enums import GatherSpanners, OffsetSpecial
@@ -219,16 +220,20 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         {0.0} <music21.stream.Measure 0 offset=0.0>
             {0.0} <music21.chord.Chord C2 A2>
 
-    This behavior can be modified by the `appendOrInsert` keyword to go against norms:
+    This behavior can be modified by the `givenElementsBehavior` keyword to go against the norm
+    of 'OFFSETS':
 
-    >>> s6 = stream.Stream([note.Note('C'), note.Note('D')], appendOrInsert='insert')
+    >>> from music21.stream.enums import GivenElementsBehavior
+    >>> s6 = stream.Stream([note.Note('C'), note.Note('D')],
+    ...                    givenElementsBehavior=GivenElementsBehavior.INSERT)
     >>> s6.show('text')  # all notes at offset 0.0
     {0.0} <music21.note.Note C>
     {0.0} <music21.note.Note D>
 
     >>> p1 = stream.Part(stream.Measure(note.Note('C')), id='p1')
     >>> p2 = stream.Part(stream.Measure(note.Note('D')), id='p2')
-    >>> s7 = stream.Score([p1, p2], appendOrInsert='append')
+    >>> s7 = stream.Score([p1, p2],
+    ...                   givenElementsBehavior=GivenElementsBehavior.APPEND)
     >>> s7.show('text')  # parts following each other (not recommended)
     {0.0} <music21.stream.Part p1>
         {0.0} <music21.stream.Measure 0 offset=0.0>
@@ -237,7 +242,6 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         {0.0} <music21.stream.Measure 0 offset=0.0>
             {0.0} <music21.note.Note D>
 
-
     For developers of subclasses, please note that because of how Streams
     are copied, there cannot be
     required parameters (i.e., without defaults) in initialization.
@@ -249,14 +253,14 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
                 ...
 
     New in v.7 -- smart appending
-    New in v.8 -- appendOrInsert keyword configures the smart appending.
+    New in v.8 -- givenElementsBehavior keyword configures the smart appending.
     '''
     # this static attributes offer a performance boost over other
     # forms of checking class
     isStream = True
     isMeasure = False
     classSortOrder: t.Union[int, float] = -20
-    recursionType = 'elementsFirst'
+    recursionType: RecursionType = RecursionType.ELEMENTS_FIRST
 
     _styleClass = style.StreamStyle
 
@@ -271,7 +275,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         'recursionType': '''
             Class variable:
 
-            String of ('elementsFirst' (default), 'flatten', 'elementsOnly)
+            RecursionType Enum of (ELEMENTS_FIRST (default), FLATTEN, ELEMENTS_ONLY)
             that decides whether the stream likely holds relevant
             contexts for the elements in it.
 
@@ -313,7 +317,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
                                         base.Music21Object,
                                         t.Sequence[base.Music21Object]] = None,
                  *,
-                 appendOrInsert: t.Literal['append', 'insert', 'offsets'] = 'offsets',
+                 givenElementsBehavior: GivenElementsBehavior = GivenElementsBehavior.OFFSETS,
                  **keywords):
         # restrictClass: t.Type[M21ObjType] = base.Music21Object,
         super().__init__(**keywords)
@@ -347,7 +351,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         # (i.e. Part or Voice generally, but even Opus theoretically)
         # because these classes usually represent synchrony
         appendBool = True
-        if appendOrInsert == 'offsets':
+        if givenElementsBehavior == GivenElementsBehavior.OFFSETS:
             try:
                 appendBool = all(e.offset == 0.0 for e in givenElements)
             except AttributeError:
@@ -356,7 +360,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
                     (e.isStream and e.classSet.isdisjoint((Measure, Score)))
                     for e in givenElements):
                 appendBool = False
-        elif appendOrInsert == 'insert':
+        elif givenElementsBehavior == GivenElementsBehavior.INSERT:
             appendBool = False
         else:
             appendBool = True
@@ -12681,10 +12685,9 @@ class Voice(Stream):
 
     Voices have a sort order of 1 greater than time signatures
     '''
-    recursionType = 'elementsFirst'
+    recursionType = RecursionType.ELEMENTS_FIRST
     classSortOrder = 5
 
-
 # -----------------------------------------------------------------------------
 
 
@@ -12724,7 +12727,7 @@ class Measure(Stream):
     key or timeSignature than previous measures, allow for padding (and pickups),
     and can be found as a "measure slice" within a score and parts.
     '''
-    recursionType = 'elementsFirst'
+    recursionType = RecursionType.ELEMENTS_FIRST
     isMeasure = True
 
     # define order for presenting names in documentation; use strings
@@ -13354,7 +13357,7 @@ class Part(Stream):
     assumes that this part fits on one staff and shares it with no other
     part
     '''
-    recursionType = 'flatten'
+    recursionType = RecursionType.FLATTEN
 
     # _DOC_ATTR: t.Dict[str, str] = {
     # }
@@ -13589,7 +13592,7 @@ class Score(Stream):
     but we figure that many people will like calling the largest
     container a Score and that this will become a standard.
     '''
-    recursionType = 'elementsOnly'
+    recursionType = RecursionType.ELEMENTS_ONLY
 
     @property
     def parts(self) -> iterator.StreamIterator[Part]:
@@ -14083,7 +14086,7 @@ class Opus(Stream):
 
     Opus objects can contain multiple Score objects, or even other Opus objects!
     '''
-    recursionType = 'elementsOnly'
+    recursionType = RecursionType.ELEMENTS_ONLY
 
     # TODO: get by title, possibly w/ regex
 
diff --git a/music21/stream/enums.py b/music21/stream/enums.py
index 4aebec5da..9ce179275 100644
--- a/music21/stream/enums.py
+++ b/music21/stream/enums.py
@@ -9,6 +9,7 @@
 # License:      BSD, see license.txt
 # -----------------------------------------------------------------------------
 import enum
+from music21.common.enums import StrEnum
 
 class StaffType(enum.Enum):
     '''
@@ -39,6 +40,18 @@ class StaffType(enum.Enum):
     OTHER = 'other'
 
 
+class GivenElementsBehavior(StrEnum):
+    APPEND = 'append'
+    OFFSETS = 'offsets'
+    INSERT = 'insert'
+
+
+class RecursionType(StrEnum):
+    ELEMENTS_FIRST = 'elementsFirst'
+    FLATTEN = 'flatten'
+    ELEMENTS_ONLY = 'elementsOnly'
+
+
 if __name__ == '__main__':
     from music21 import mainTest
     mainTest()
diff --git a/music21/variant.py b/music21/variant.py
index 1e73074cc..d60defca2 100644
--- a/music21/variant.py
+++ b/music21/variant.py
@@ -35,6 +35,7 @@ from music21 import meter
 from music21 import note
 from music21 import search
 from music21 import stream
+from music21.stream.enums import GivenElementsBehavior
 
 environLocal = environment.Environment('variant')
 
@@ -95,13 +96,13 @@ class Variant(base.Music21Object):
                                base.Music21Object,
                                t.Sequence[base.Music21Object]] = None,
         name: t.Optional[str] = None,
-        appendOrInsert: t.Literal['append', 'insert', 'offsets'] = 'offsets',
+        givenElementsBehavior: GivenElementsBehavior = GivenElementsBehavior.OFFSETS,
         **music21ObjectKeywords,
     ):
         super().__init__(**music21ObjectKeywords)
         self.exposeTime = False
         self._stream = stream.VariantStorage(givenElements=givenElements,
-                                             appendOrInsert=appendOrInsert)
+                                             givenElementsBehavior=givenElementsBehavior)
 
         self._replacementDuration = None
 


Test patch:
diff --git a/music21/test/commonTest.py b/music21/test/commonTest.py
index cdda7b748..a19b516a0 100644
--- a/music21/test/commonTest.py
+++ b/music21/test/commonTest.py
@@ -224,8 +224,8 @@ class ModuleGather:
                             'analysis/windowed',
                             'converter/__init__',
 
-                            'musicxml/m21ToXml',
-                            'musicxml/xmlToM21',
+                            'musicxml/test_m21ToXml',
+                            'musicxml/test_xmlToM21',
 
                             'romanText/translate',
                             'corpus/testCorpus',
diff --git a/music21/test/test_base.py b/music21/test/test_base.py
index 9bcb727f3..f0e0d0873 100644
--- a/music21/test/test_base.py
+++ b/music21/test/test_base.py
@@ -948,10 +948,12 @@ class Test(unittest.TestCase):
         for y in n.contextSites():
             yTup = (y.site, y.offset, y.recurseType)
             siteList.append(repr(yTup))
-        self.assertEqual(siteList,
-                         ["(<music21.stream.Measure 3 offset=9.0>, 0.5, 'elementsFirst')",
-                          "(<music21.stream.Part Alto>, 9.5, 'flatten')",
-                          "(<music21.stream.Score bach>, 9.5, 'elementsOnly')"])
+        self.assertEqual(
+            siteList,
+            ['(<music21.stream.Measure 3 offset=9.0>, 0.5, <RecursionType.ELEMENTS_FIRST>)',
+             '(<music21.stream.Part Alto>, 9.5, <RecursionType.FLATTEN>)',
+             '(<music21.stream.Score bach>, 9.5, <RecursionType.ELEMENTS_ONLY>)']
+        )
 
         m = c[2][4]
         self.assertEqual(repr(m), '<music21.stream.Measure 3 offset=9.0>')
@@ -960,10 +962,12 @@ class Test(unittest.TestCase):
         for y in m.contextSites():
             yTup = (y.site, y.offset, y.recurseType)
             siteList.append(repr(yTup))
-        self.assertEqual(siteList,
-                         ["(<music21.stream.Measure 3 offset=9.0>, 0.0, 'elementsFirst')",
-                          "(<music21.stream.Part Alto>, 9.0, 'flatten')",
-                          "(<music21.stream.Score bach>, 9.0, 'elementsOnly')"])
+        self.assertEqual(
+            siteList,
+            ['(<music21.stream.Measure 3 offset=9.0>, 0.0, <RecursionType.ELEMENTS_FIRST>)',
+             '(<music21.stream.Part Alto>, 9.0, <RecursionType.FLATTEN>)',
+             '(<music21.stream.Score bach>, 9.0, <RecursionType.ELEMENTS_ONLY>)']
+        )
 
         m2 = copy.deepcopy(m)
         m2.number = 3333
@@ -971,10 +975,12 @@ class Test(unittest.TestCase):
         for y in m2.contextSites():
             yTup = (y.site, y.offset, y.recurseType)
             siteList.append(repr(yTup))
-        self.assertEqual(siteList,
-                         ["(<music21.stream.Measure 3333 offset=0.0>, 0.0, 'elementsFirst')",
-                          "(<music21.stream.Part Alto>, 9.0, 'flatten')",
-                          "(<music21.stream.Score bach>, 9.0, 'elementsOnly')"])
+        self.assertEqual(
+            siteList,
+            ['(<music21.stream.Measure 3333 offset=0.0>, 0.0, <RecursionType.ELEMENTS_FIRST>)',
+             '(<music21.stream.Part Alto>, 9.0, <RecursionType.FLATTEN>)',
+             '(<music21.stream.Score bach>, 9.0, <RecursionType.ELEMENTS_ONLY>)']
+        )
         siteList = []
 
         cParts = c.parts.stream()  # need this otherwise it could possibly be garbage collected.
@@ -986,11 +992,13 @@ class Test(unittest.TestCase):
             yTup = (y.site, y.offset, y.recurseType)
             siteList.append(repr(yTup))
 
-        self.assertEqual(siteList,
-                         ["(<music21.stream.Measure 3 offset=9.0>, 0.0, 'elementsFirst')",
-                          "(<music21.stream.Part Alto>, 9.0, 'flatten')",
-                          "(<music21.stream.Score partStream>, 9.0, 'elementsOnly')",
-                          "(<music21.stream.Score bach>, 9.0, 'elementsOnly')"])
+        self.assertEqual(
+            siteList,
+            ['(<music21.stream.Measure 3 offset=9.0>, 0.0, <RecursionType.ELEMENTS_FIRST>)',
+             '(<music21.stream.Part Alto>, 9.0, <RecursionType.FLATTEN>)',
+             '(<music21.stream.Score partStream>, 9.0, <RecursionType.ELEMENTS_ONLY>)',
+             '(<music21.stream.Score bach>, 9.0, <RecursionType.ELEMENTS_ONLY>)']
+        )
 
     def testContextSitesB(self):
         p1 = stream.Part()