Repository: cuthbertLab/music21
Pull request #1466 (2022-10-17 00:57:04)

Equality changes -- hierarchy
* Add an equality hierarchy to music21 to enable easier development (needed for @MarkGotham PRs)
* Add documentation for how equality and hashing work in music21
* Add notes in dev documents about equality and hashing in music21 and why there are mistakes that can't be fixed.
* Before there was a mishmash of which Music21Objects could be hashed.  Aim for all to be able to -- with consequences on devs if they mutate them later.
* Use deque in a few places where pop(0) was being used.  Remove pop(False)



Patch:
diff --git a/documentation/source/developerReference/index.rst b/documentation/source/developerReference/index.rst
index f945044dd..db78f19b6 100644
--- a/documentation/source/developerReference/index.rst
+++ b/documentation/source/developerReference/index.rst
@@ -11,6 +11,8 @@ Developer Reference
 
     usingGit
     musicxmlTest
+    
+    startingOver
 
 
 Documentation and tests in progress
diff --git a/documentation/source/developerReference/startingOver.ipynb b/documentation/source/developerReference/startingOver.ipynb
new file mode 100644
index 000000000..5fbd6d45a
--- /dev/null
+++ b/documentation/source/developerReference/startingOver.ipynb
@@ -0,0 +1,55 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "45a1e554",
+   "metadata": {},
+   "source": [
+    "# Music21's Mistakes\n",
+    "\n",
+    "So, you're a developer.  Maybe you hate Python or you hate `music21` or you think (whenever you read this) that `music21` was cool for its time, but it's time to create a new system from scratch.\n",
+    "\n",
+    "I'm never going to do that: I know about the [Second System Effect](https://en.wikipedia.org/wiki/Second-system_effect) and know that I couldn't do it right.\n",
+    "\n",
+    "But if you're interested, here are the mistakes I made in `music21` that I know of that are too late to change now, but that I'd avoid if I were you:\n",
+    "\n",
+    "1. Most objects should be immutable.  Maybe Streams can't be, but definitely Pitches, Accidentals, Durations, etc. Then they would be hashable and equality operators would work the way Python and most other languages intend them to be.  But as it is, we have a situation where we need to know that Streams compare on identity while lists of notes compare on equality.  If you don't have a handle on the difference between the identity and equality, do yourself a favor and become an expert before writing your system.  I didn't know and now things are awful in this respect.\n",
+    "\n",
+    "2. Because of point 1, I probably would store objects and sites separately and return them as a separate composite object; this would eliminate 90% of the deepcopying happening in `music21`.\n",
+    "\n",
+    "3. I borrowed `-` for flat from Humdrum thinking that there would otherwise be a lot of conflicts between octave signatures for B, like 'BBB' and 'bbb' and flats.  It turns out that hasn't been much of a problem.\n",
+    "\n",
+    "4. Follow Liskov Substitution Principle.  Violations of it are all over music21, and that's been an issue as computers need to infer type better. \n",
+    "\n",
+    "5. Use fewer properties except when they are fast to compute, and never let a property have a side effect.  Modern IDEs are constantly querying all properties to see what that might contain.  This is why `.flat` had to become `.flatten()`.\n",
+    "\n",
+    "6. Keep the core of the toolkit small and encourage an ecosystem of other add-ons.\n",
+    "\n",
+    "7. Don't try to encode every exception to standard music that your musicologist brain can conceive of.  Put those in the `.editorial` or other special purpose attribute.  \n",
+    "\n",
+    "There will be more here as I think of it.  -- Michael Scott Asato Cuthbert, 2022 October."
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/music21/analysis/reduceChords.py b/music21/analysis/reduceChords.py
index 3479629d5..d79c033d8 100644
--- a/music21/analysis/reduceChords.py
+++ b/music21/analysis/reduceChords.py
@@ -181,7 +181,7 @@ class ChordReducer:
         return frozenset(result)
 
     def _iterateElementsPairwise(self, inputStream):
-        elementBuffer = []
+        elementBuffer = collections.deque()
         prototype = (
             chord.Chord,
             note.Note,
@@ -193,7 +193,7 @@ class ChordReducer:
             elementBuffer.append(element)
             if len(elementBuffer) == 2:
                 yield tuple(elementBuffer)
-                elementBuffer.pop(0)
+                elementBuffer.popleft()
 
     # PUBLIC METHODS #
     def alignHockets(self, scoreTree):
diff --git a/music21/articulations.py b/music21/articulations.py
index 24e2a0547..147ec7153 100644
--- a/music21/articulations.py
+++ b/music21/articulations.py
@@ -104,6 +104,41 @@ class Articulation(base.Music21Object):
     >>> x.style.absoluteY = 20
     >>> x.displayText = '>'
 
+    Equality
+    --------
+    Equality of articulations is based only on the class, as other attributes are independent
+    of context and deployment.
+
+    >>> at1 = articulations.StrongAccent()
+    >>> at2 = articulations.StrongAccent()
+    >>> at1.placement = 'above'
+    >>> at2.placement = 'below'
+    >>> at1 == at2
+    True
+
+    Comparison between classes and with the object itself behaves as expected:
+
+    >>> at3 = articulations.Accent()
+    >>> at4 = articulations.Staccatissimo()
+    >>> at1 == at3
+    False
+    >>> at4 == at4
+    True
+
+    OMIT_FROM_DOCS
+
+    >>> at5 = articulations.Staccato()
+    >>> at6 = articulations.Spiccato()
+    >>> [at1, at4, at3] == [at1, at4, at3]
+    True
+    >>> [at1, at2, at3] == [at2, at3, at1]
+    False
+    >>> {at1, at2, at3} == {at2, at3, at1}
+    True
+    >>> at6 == True
+    False
+
+    This is in OMIT
     '''
     _styleClass: type[style.Style] = style.TextStyle
 
@@ -138,52 +173,6 @@ class Articulation(base.Music21Object):
         className = self.__class__.__name__
         return common.camelCaseToHyphen(className, replacement=' ')
 
-    # def __eq__(self, other):
-    #     '''
-    #     Equality. Based only on the class name,
-    #     as other attributes are independent of context and deployment.
-    #
-    #
-    #     >>> at1 = articulations.StrongAccent()
-    #     >>> at2 = articulations.StrongAccent()
-    #     >>> at1.placement = 'above'
-    #     >>> at2.placement = 'below'
-    #     >>> at1 == at2
-    #     True
-    #
-    #
-    #     Comparison between classes and with the object itself behaves as expected
-    #
-    #
-    #     >>> at3 = articulations.Accent()
-    #     >>> at4 = articulations.Staccatissimo()
-    #     >>> at1 == at3
-    #     False
-    #     >>> at4 == at4
-    #     True
-    #
-    #
-    #     OMIT_FROM_DOCS
-    #
-    #     >>> at5 = articulations.Staccato()
-    #     >>> at6 = articulations.Spiccato()
-    #     >>> [at1, at4, at3] == [at1, at4, at3]
-    #     True
-    #     >>> [at1, at2, at3] == [at2, at3, at1]
-    #     False
-    #     >>> set([at1, at2, at3]) == set([at2, at3, at1])
-    #     True
-    #     >>> at6 == None
-    #     False
-    #     '''
-    #     # checks pitch.octave, pitch.accidental, uses Pitch.__eq__
-    #     if other == None or not isinstance(other, Articulation):
-    #         return False
-    #     elif self.__class__ == other.__class__:
-    #         return True
-    #     return False
-    #
-
     def _getVolumeShift(self):
         return self._volumeShift
 
diff --git a/music21/bar.py b/music21/bar.py
index cc774a0bb..fa6e17c19 100644
--- a/music21/bar.py
+++ b/music21/bar.py
@@ -128,6 +128,8 @@ class Barline(base.Music21Object):
 
     classSortOrder = -5
 
+    equalityAttributes = ('type', 'pause', 'location')
+
     def __init__(self,
                  type=None,  # pylint: disable=redefined-builtin
                  location=None,
diff --git a/music21/base.py b/music21/base.py
index 7c5916765..6c27b7d3a 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -40,6 +40,7 @@ from __future__ import annotations
 import builtins
 from collections.abc import Generator, Iterable
 import copy
+import functools
 from importlib.util import find_spec
 import typing as t
 from typing import overload  # Pycharm can't do alias
@@ -74,7 +75,7 @@ if TYPE_CHECKING:
     from music21 import stream
     from music21 import spanner
 
-_M21T = t.TypeVar('_M21T', bound='music21.base.Music21Object')
+    _M21T = t.TypeVar('_M21T', bound='music21.base.Music21Object')
 
 # all other music21 modules below...
 
@@ -177,6 +178,8 @@ class Groups(list):  # no need to inherit from slotted object
     Groups is a list (subclass) of strings used to identify
     associations that an element might have.
 
+    (in the future, Groups will become a set subclass)
+
     The Groups object enforces that all elements must be strings, and that
     the same element cannot be provided more than once.
 
@@ -269,9 +272,44 @@ _EQUALITY_SENTINEL_SELF = object()
 _EQUALITY_SENTINEL_OTHER = object()
 
 
+@functools.cache
+def _getEqualityAttributes(cls) -> frozenset[str]:
+    '''
+    Get equality attributes for a class.  Cached.
+
+    >>> base._getEqualityAttributes(base.Music21Object)
+    frozenset({'duration'})
+    >>> 'location' in base._getEqualityAttributes(bar.Barline)
+    True
+    >>> 'pitch' in base._getEqualityAttributes(bar.Barline)
+    False
+
+    '''
+    equalityAttributes = set()
+    # equalityAttributesIgnore works, but not yet needed.
+    # equalityAttributesIgnore = set()
+    # for klass in [cls, *cls.mro()]:
+    #     if hasattr(klass, 'equalityAttributesIgnore'):
+    #         ka = klass.equalityAttributesIgnore
+    #         if isinstance(ka, str):  # mistake.  Happens TOO often:
+    #             ka = (ka,)
+    #         equalityAttributesIgnore |= set(ka)
+
+    for klass in [cls, *cls.mro()]:
+        if hasattr(klass, 'equalityAttributes'):
+            ka = klass.equalityAttributes
+            if isinstance(ka, str):  # mistake.  Happens TOO often:
+                ka = (ka,)
+            equalityAttributes |= set(ka)
+    # equalityAttributes.difference_update(equalityAttributesIgnore)
+    return frozenset(equalityAttributes)
+
+
 class Music21Object(prebase.ProtoM21Object):
     '''
-    Base class for all music21 objects.
+    Music21Object is the base class for all elements that can go into Streams.
+    Notes, Clefs, TimeSignatures are all sublcasses of Music21Object.  Durations
+    and Pitches (which need to be attached to Notes, etc.) are not.
 
     All music21 objects have these pieces of information:
 
@@ -280,9 +318,9 @@ class Music21Object(prebase.ProtoM21Object):
     2.  groups: a :class:`~music21.base.Groups` object: which is a
         list of strings identifying internal sub-collections
         (voices, parts, selections) to which this element belongs
-    3.  duration: Duration object representing the length of the object
-    4.  activeSite: a reference to the currently active Stream or None
-    5.  offset: a floating point value, generally in quarter lengths,
+    3.  duration: :class:`~music21.duration.Duration` object representing the length of the object
+    4.  activeSite: a reference to the currently active :class:`~music21.stream.Stream` or None
+    5.  offset: a floating point or Fraction value, generally in quarter lengths,
         specifying the position of the object in a site.
     6.  priority: int representing the position of an object among all
         objects at the same offset.
@@ -296,11 +334,79 @@ class Music21Object(prebase.ProtoM21Object):
         if that is not desired.
     10. editorial: a :class:`~music21.editorial.Editorial` object
 
-
     Each of these may be passed in as a named keyword to any music21 object.
 
     Some of these may be intercepted by the subclassing object (e.g., duration
     within Note)
+
+    Equality
+    --------
+    For historical reasons, music21 uses a different idea of object equality
+    for Music21Objects than recommended by modern Python standards.
+
+    Two Music21Objects are equal if they are the same class and same duration.
+
+    Their offset, activeSite, id, and groups do not matter for equality.
+
+    Since these two objects are therefore not interchangable, they do not have
+    the same hash value.
+
+    >>> obj1 = base.Music21Object(id='obj1')
+    >>> obj2 = base.Music21Object(id='obj2')
+    >>> obj1 == obj2
+    True
+    >>> hash(obj1) == hash(obj2)
+    False
+
+    This has the stange side effect that structures that use equality to
+    report containment (such as lists and tuples) will report differently from
+    structures that use hash values to report containment (such as dicts and sets):
+
+    >>> obj1 in [obj2]
+    True
+    >>> obj1 in {obj2}
+    False
+
+    Subclasses need to apply stricter criteria for equality, like Barline does here
+    with `.location`
+
+    >>> bar1 = bar.Barline('double', 'left')
+    >>> bar2 = bar.Barline('double', 'right')
+    >>> bar1 == bar2
+    False
+    >>> bar2.location = 'left'
+    >>> bar1 == bar2
+    True
+    >>> bar1.duration.type = 'whole'  # Buh?
+    >>> bar1 == bar2
+    False
+
+    In general, a subclass of Music21Object must match all super-class criteria for
+    equality before they can be considered equal themselves.  However, there are some
+    exceptions.  For instance, RomanNumeral objects with the same figure and key are
+    equal even if their notes are in different octaves or have different doublings.
+
+    Developers creating their own Music21Object subclasses should add a class attribute
+    `equalityAttributes = ('one', 'two')`.  (Remember that as a tuple of strings, if there
+    is only one string, don't forget the trailing comma: `('only',)`.
+
+    >>> class CarolineShawBreathMark(base.Music21Object):
+    ...     equalityAttributes = ('direction',)
+    ...     def __init__(self, direction, speed):
+    ...         super().__init__(self)
+    ...         self.direction = direction
+    ...         self.speed = speed
+    >>> bm1 = CarolineShawBreathMark('in', 'fast')
+    >>> bm2 = CarolineShawBreathMark('out', 'fast')
+    >>> bm1 == bm2
+    False
+
+    "speed" is not in the equalityAttributes so it can differ while objects are still
+    equal.
+
+    >>> bm3 = CarolineShawBreathMark('in', 'slow')
+    >>> bm1 == bm3
+    True
     '''
 
     classSortOrder: int | float = 20  # default classSortOrder
@@ -309,7 +415,8 @@ class Music21Object(prebase.ProtoM21Object):
 
     _styleClass: type[Style] = Style
 
-    equalityAttributes = ('duration',)
+    equalityAttributes: tuple[str, ...] = ('duration',)
+    # equalityAttributesIgnore: tuple[str, ...] = ()  # this must be defined anew in each subclass.
 
     # define order for presenting names in documentation; use strings
     _DOC_ORDER: list[str] = []
@@ -406,11 +513,25 @@ class Music21Object(prebase.ProtoM21Object):
         if quarterLength is not None:
             self.duration.quarterLength = quarterLength
 
-    # def __eq__(self, other):
-    #     '''
-    #     Two music21 objects are
-    #     '''
+    def __eq__(self: _M21T, other) -> t.TypeGuard[_M21T]:
+        '''
+        Define equality for Music21Objects.  See main class docs.
+        '''
+        cls = t.cast(type, self.__class__)
+        if not isinstance(other, cls):
+            return False
 
+        for attr in _getEqualityAttributes(cls):
+            if (getattr(self, attr, _EQUALITY_SENTINEL_SELF)
+                    != getattr(other, attr, _EQUALITY_SENTINEL_OTHER)):
+                return False
+        return True
+
+    def __hash__(self) -> int:
+        '''
+        Restore hashing, but only on id(self)
+        '''
+        return id(self) >> 4
 
     @property
     def id(self) -> int | str:
@@ -436,7 +557,7 @@ class Music21Object(prebase.ProtoM21Object):
             warnings.warn(msg)
         self._id = new_id
 
-    def mergeAttributes(self, other: 'Music21Object') -> None:
+    def mergeAttributes(self, other: Music21Object) -> None:
         '''
         Merge all elementary, static attributes. Namely,
         `id` and `groups` attributes from another music21 object.
@@ -1038,55 +1159,55 @@ class Music21Object(prebase.ProtoM21Object):
         Stream subclass, SpannerStorage, internally to keep track
         of the elements that are spanned.
 
-        >>> n1 = note.Note('C4')
-        >>> n2 = note.Note('D4')
-        >>> sp1 = spanner.Slur(n1, n2)
-        >>> n1.getSpannerSites() == [sp1]
+        >>> c = note.Note('C4')
+        >>> d = note.Note('D4')
+        >>> slur1 = spanner.Slur(c, d)
+        >>> c.getSpannerSites() == [slur1]
         True
 
         Note that not all Spanners are in the spanner module. They
         tend to reside in modules closer to their musical function:
 
-        >>> sp2 = dynamics.Crescendo(n2, n1)
+        >>> cresc = dynamics.Crescendo(d, c)
 
-        The order that Spanners are returned is usually the order they
-        were created, but on fast computers there can be ties, so use
-        a set comparison if you expect multiple:
+        The order that Spanners are returned is by sortTuple.  For spanners
+        created the same way and in the same order, the order returned will
+        be consistent:
 
-        >>> set(n2.getSpannerSites()) == {sp1, sp2}
+        >>> d.getSpannerSites() == [slur1, cresc]
         True
 
         Optionally a class name or list of class names (as Classes or strings)
         can be specified and only Spanners of that class will be returned
 
-        >>> sp3 = dynamics.Diminuendo(n1, n2)
-        >>> n2.getSpannerSites(dynamics.Diminuendo) == [sp3]
+        >>> dim = dynamics.Diminuendo(c, d)
+        >>> d.getSpannerSites(dynamics.Diminuendo) == [dim]
         True
 
         A larger class name can be used to get all subclasses:
 
-        >>> set(n2.getSpannerSites(dynamics.DynamicWedge)) == {sp2, sp3}
+        >>> d.getSpannerSites(dynamics.DynamicWedge) == [cresc, dim]
         True
-        >>> set(n2.getSpannerSites(['Slur', 'Diminuendo'])) == {sp1, sp3}
+        >>> d.getSpannerSites(['Slur', 'Diminuendo']) == [slur1, dim]
         True
 
         Note that the order of spanners returned from this routine can vary, so
-        changing to a set is useful:
+        changing to a set is useful for comparisons
 
-        >>> set(n2.getSpannerSites(['Slur', 'Diminuendo'])) == {sp3, sp1}
+        >>> set(d.getSpannerSites(['Slur', 'Diminuendo'])) == {dim, slur1}
         True
 
 
         Example: see which pairs of notes are in the same slur.
 
-        >>> n3 = note.Note('E4')
-        >>> sp4 = spanner.Slur(n1, n3)
+        >>> e = note.Note('E4')
+        >>> slur2 = spanner.Slur(c, e)
 
-        >>> for n in [n1, n2, n3]:
-        ...    for nOther in [n1, n2, n3]:
+        >>> for n in [c, d, e]:
+        ...    nSlurs = n.getSpannerSites(spanner.Slur)
+        ...    for nOther in [c, d, e]:
         ...        if n is nOther:
         ...            continue
-        ...        nSlurs = n.getSpannerSites(spanner.Slur)
         ...        nOtherSlurs = nOther.getSpannerSites(spanner.Slur)
         ...        for thisSlur in nSlurs:
         ...            if thisSlur in nOtherSlurs:
@@ -1097,7 +1218,7 @@ class Music21Object(prebase.ProtoM21Object):
         E shares a slur with C
         '''
         found = self.sites.getSitesByClass('SpannerStorage')
-        post = []
+        post: list[spanner.Spanner] = []
         if spannerClassList is not None:
             if not common.isIterable(spannerClassList):
                 spannerClassList = [spannerClassList]
@@ -1113,7 +1234,7 @@ class Music21Object(prebase.ProtoM21Object):
                         post.append(obj.client)
                         break
 
-        return post
+        return sorted(post, key=lambda x: x.sortTuple())
 
     def purgeOrphans(self, excludeStorageStreams=True) -> None:
         '''
@@ -4000,7 +4121,7 @@ class ElementWrapper(Music21Object):
         else:
             return f'offset={self.offset} obj={shortObj!r}'
 
-    def __eq__(self, other) -> bool:
+    def __eq__(self, other):
         '''
         Test ElementWrapper equality
 
diff --git a/music21/beam.py b/music21/beam.py
index 7f3925647..3420b4b97 100644
--- a/music21/beam.py
+++ b/music21/beam.py
@@ -239,6 +239,9 @@ class Beams(prebase.ProtoM21Object, EqualSlottedObjectMixin):
     def __eq__(self, other):
         return isinstance(other, self.__class__) and repr(self) == repr(other)
 
+    def __hash__(self):
+        return id(self) >> 4
+
     def _reprInternal(self):
         msg = []
         for beam in self.beamsList:
diff --git a/music21/braille/segment.py b/music21/braille/segment.py
index 3247ac102..a418c1352 100644
--- a/music21/braille/segment.py
+++ b/music21/braille/segment.py
@@ -19,7 +19,7 @@ and will henceforth be referred to as BMTM.
 '''
 from __future__ import annotations
 
-import collections
+from collections import deque, namedtuple
 import copy
 import enum
 import typing as t
@@ -145,10 +145,9 @@ SEGMENT_MAXNOTESFORSHORTSLUR = 4
 
 MAX_ELEMENTS_IN_SEGMENT = 48  # 8 measures of 6 notes, etc. each
 
-_ThreeDigitNumber = collections.namedtuple('_ThreeDigitNumber', ['hundreds', 'tens', 'ones'])
+_ThreeDigitNumber = namedtuple('_ThreeDigitNumber', ['hundreds', 'tens', 'ones'])
 
-SegmentKey = collections.namedtuple('SegmentKey',
-                                    ['measure', 'ordinal', 'affinity', 'hand'])
+SegmentKey = namedtuple('SegmentKey', ['measure', 'ordinal', 'affinity', 'hand'])
 SegmentKey.__new__.__defaults__ = (0, 0, None, None)
 
 
@@ -358,7 +357,7 @@ class BrailleSegment(text.BrailleText):
         super().__init__(lineLength=lineLength)
         self._groupingDict: dict[SegmentKey, BrailleElementGrouping] = {}
 
-        self.groupingKeysToProcess: list[SegmentKey] = []
+        self.groupingKeysToProcess: deque[SegmentKey] = deque()
         self.currentGroupingKey: SegmentKey | None = None
         self.previousGroupingKey: SegmentKey | None = None
         self.lastNote = None
@@ -450,7 +449,7 @@ class BrailleSegment(text.BrailleText):
 
         Returns brailleText
         '''
-        self.groupingKeysToProcess = list(sorted(self.keys()))
+        self.groupingKeysToProcess = deque(sorted(self.keys()))
 
         if self.showHeading:
             self.extractHeading()  # Heading
@@ -463,7 +462,7 @@ class BrailleSegment(text.BrailleText):
 
         self.previousGroupingKey = None
         while self.groupingKeysToProcess:
-            self.currentGroupingKey = self.groupingKeysToProcess.pop(0)
+            self.currentGroupingKey = self.groupingKeysToProcess.popleft()
 
             cgkAffinityGroup = self.currentGroupingKey.affinity
 
@@ -534,7 +533,7 @@ class BrailleSegment(text.BrailleText):
         >>> print(seg.brailleText)
         ⠼⠙⠄
         '''
-        gkp = self.groupingKeysToProcess or sorted(self.keys())
+        gkp = self.groupingKeysToProcess or deque(sorted(self.keys()))
         firstSegmentKey = gkp[0]
         initMeasureNumber = firstSegmentKey.measure
         brailleNumber = basic.numberToBraille(initMeasureNumber)
@@ -555,12 +554,12 @@ class BrailleSegment(text.BrailleText):
         metronomeMark = None
         # find the first keySignature and timeSignature...
 
-        groupingKeysToProcess = self.groupingKeysToProcess or sorted(self.keys())
+        groupingKeysToProcess = self.groupingKeysToProcess or deque(sorted(self.keys()))
 
         while groupingKeysToProcess:
             if groupingKeysToProcess[0].affinity > Affinity.MMARK:
                 break
-            cgk = groupingKeysToProcess.pop(0)  # cgk = currentGroupingKey
+            cgk = groupingKeysToProcess.popleft()  # cgk = currentGroupingKey
 
             cgkAffinityGroup = cgk.affinity
             currentBrailleGrouping = self._groupingDict.get(cgk)  # currentGrouping...
@@ -912,9 +911,9 @@ class BrailleSegment(text.BrailleText):
         '''
         extracts a tempo text and processes it...
         '''
-        self.groupingKeysToProcess.insert(0, self.currentGroupingKey)
+        self.groupingKeysToProcess.appendleft(self.currentGroupingKey)
         if self.previousGroupingKey.affinity == Affinity.SIGNATURE:
-            self.groupingKeysToProcess.insert(0, self.previousGroupingKey)
+            self.groupingKeysToProcess.appendleft(self.previousGroupingKey)
         self.extractHeading()
         self.extractMeasureNumber()
 
@@ -1092,8 +1091,8 @@ class BrailleGrandSegment(BrailleSegment, text.BrailleKeyboard):
     def __init__(self, lineLength: int = 40):
         BrailleSegment.__init__(self, lineLength=lineLength)
         text.BrailleKeyboard.__init__(self, lineLength=lineLength)
-        self.allKeyPairs: list[tuple[SegmentKey | None,
-                                     SegmentKey | None]] = []
+        self.allKeyPairs: deque[tuple[SegmentKey | None,
+                                      SegmentKey | None]] = deque()
         self.previousGroupingPair = None
         self.currentGroupingPair = None
 
@@ -1260,7 +1259,7 @@ class BrailleGrandSegment(BrailleSegment, text.BrailleKeyboard):
         '''
         Returns the BrailleText from the combined grouping keys
         '''
-        self.allKeyPairs = list(self.yieldCombinedGroupingKeys())
+        self.allKeyPairs = deque(self.yieldCombinedGroupingKeys())
         lastPair = self.allKeyPairs[-1]
         highestMeasure = lastPair[0].measure if lastPair[0] else lastPair[1].measure
         self.highestMeasureNumberLength = len(str(highestMeasure))
@@ -1269,7 +1268,7 @@ class BrailleGrandSegment(BrailleSegment, text.BrailleKeyboard):
         self.currentGroupingPair = None
         while self.allKeyPairs:
             self.previousGroupingPair = self.currentGroupingPair
-            self.currentGroupingPair = self.allKeyPairs.pop(0)
+            self.currentGroupingPair = self.allKeyPairs.popleft()
             (rightKey, leftKey) = self.currentGroupingPair
 
             if ((rightKey is not None and rightKey.affinity >= Affinity.INACCORD)
@@ -1295,7 +1294,7 @@ class BrailleGrandSegment(BrailleSegment, text.BrailleKeyboard):
         tempoText = None
         metronomeMark = None
 
-        while True:
+        while self.allKeyPairs:
             (rightKey, leftKey) = self.allKeyPairs[0]
             useKey = rightKey
             try:
@@ -1308,7 +1307,7 @@ class BrailleGrandSegment(BrailleSegment, text.BrailleKeyboard):
                     raise ke
             if useKey.affinity > Affinity.MMARK:
                 break
-            self.allKeyPairs.pop(0)
+            self.allKeyPairs.popleft()
             if useKey.affinity == Affinity.SIGNATURE:
                 try:
                     keySignature, timeSignature = useElement[0], useElement[1]
diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py
index 5e67ceee3..ad95eb94a 100644
--- a/music21/chord/__init__.py
+++ b/music21/chord/__init__.py
@@ -63,6 +63,31 @@ class ChordBase(note.NotRest):
 
     As of Version 7, ChordBase lies between Chord and NotRest in the music21
     hierarchy, so that features can be shared with PercussionChord.
+
+    >>> cb = chord.ChordBase('C4 E4 G4')
+    >>> cb.notes
+    (<music21.note.Note C>, <music21.note.Note E>, <music21.note.Note G>)
+
+
+    Equality
+    --------
+    Equality on ChordBase is strange, but necessary to help Chord and PercussionChord
+    do meaningful equality checks themselves.
+
+    Two ChordBase objects are equal if they pass all `super()`
+    equality tests and the **number** of stored Notes are the same.
+
+    >>> cb1 = chord.ChordBase('C4 E4 G4')
+    >>> cb2 = chord.ChordBase('C4 E4')
+    >>> cb1 == cb2
+    False
+
+    This is surprising, but it's necessary to make checking equality
+    of Chord objects and PercussionChord objects themselves easier.
+
+    >>> cb3 = chord.ChordBase('A#4 A#4 A#4')
+    >>> cb1 == cb3
+    True
     '''
     isNote = False
     isRest = False
@@ -130,36 +155,15 @@ class ChordBase(note.NotRest):
 
 
     def __eq__(self, other):
-        '''
-        True if the ChordBase passes all `super()`
-        equality tests and the pitches
-        (or display pitches, for Unpitched objects)
-        are the same (but possibly in a different order).
-
-        >>> c1 = chord.Chord('C4 E4 G4')
-        >>> c2 = chord.Chord('E4 C4 G4')
-        >>> c1 == c2
-        True
-        >>> c3 = chord.Chord('E4 C#4 G4')
-        >>> c2 == c3
-        False
-        >>> n1 = note.Note('C4')
-        >>> c1 == n1
-        False
-        >>> c2.duration.quarterLength = 2.0
-        >>> c1 == c2
-        False
-        >>> c1 != c2
-        True
-        '''
         if not super().__eq__(other):
             return False
-        if not hasattr(other, 'notes'):
-            return False
         if not len(self.notes) == len(other.notes):
             return False
         return True
 
+    def __hash__(self):
+        return super().__hash__()
+
     def __deepcopy__(self: _ChordBaseType, memo=None) -> _ChordBaseType:
         '''
         As Chord objects have one or more Volume, objects, and Volume
@@ -386,7 +390,7 @@ class ChordBase(note.NotRest):
 
     @property
     def notes(self) -> tuple[note.NotRest, ...]:
-        return ()
+        return tuple(self._notes)
 
     @property
     def tie(self) -> tie.Tie | None:
@@ -707,6 +711,28 @@ class Chord(ChordBase):
     Traceback (most recent call last):
     music21.chord.ChordException: Could not process input
                                     argument <module 'music21.base' from '...base...'>
+
+    Equality
+    --------
+    Two chords are equal if the Chord passes all `super()`
+    equality tests and all their pitches are equal
+    (possibly in a different order)
+
+    >>> c1 = chord.Chord('C4 E4 G4')
+    >>> c2 = chord.Chord('E4 C4 G4')
+    >>> c1 == c2
+    True
+    >>> c3 = chord.Chord('E4 C#4 G4')
+    >>> c2 == c3
+    False
+    >>> n1 = note.Note('C4')
+    >>> c1 == n1
+    False
+    >>> c2.duration.quarterLength = 2.0
+    >>> c1 == c2
+    False
+    >>> c1 != c2
+    True
     '''
     # CLASS VARIABLES #
     isChord = True
@@ -749,35 +775,15 @@ class Chord(ChordBase):
     # SPECIAL METHODS #
 
     def __eq__(self, other):
-        '''
-        True if the Chord passes all `super()`
-        equality tests and the pitches are the same
-        (possibly in a different order)
-
-        >>> c1 = chord.Chord('C4 E4 G4')
-        >>> c2 = chord.Chord('E4 C4 G4')
-        >>> c1 == c2
-        True
-        >>> c3 = chord.Chord('E4 C#4 G4')
-        >>> c2 == c3
-        False
-        >>> n1 = note.Note('C4')
-        >>> c1 == n1
-        False
-        >>> c2.duration.quarterLength = 2.0
-        >>> c1 == c2
-        False
-        >>> c1 != c2
-        True
-        '''
-        if super().__eq__(other) is NotImplemented:
-            return NotImplemented
         if not super().__eq__(other):
             return False
         if set(self.pitches) != set(other.pitches):
             return False
         return True
 
+    def __hash__(self):
+        return super().__hash__()
+
     def __getitem__(self, key: int | str | note.Note | pitch.Pitch):
         '''
         Get item makes accessing pitch components for the Chord easier
diff --git a/music21/clef.py b/music21/clef.py
index a6a850c35..2ef322323 100644
--- a/music21/clef.py
+++ b/music21/clef.py
@@ -64,7 +64,31 @@ class Clef(base.Music21Object):
 
     >>> tc.lowestLine
     31
+
+    Equality
+    --------
+    Two Clefs are equal if their class is the same, their sign is the same,
+    their line is the same and their octaveChange is the same.
+
+    >>> c1 = clef.PercussionClef()
+    >>> c2 = clef.NoClef()
+    >>> c1 == c2
+    False
+    >>> c3 = clef.TrebleClef()
+    >>> c4 = clef.TrebleClef()
+    >>> c3 == c4
+    True
+    >>> c4.octaveChange = -1
+    >>> c3 == c4
+    False
+
+    Note that these are not equal:
+
+    >>> clef.TrebleClef() == clef.GClef(line=2)
+    False
     '''
+    equalityAttributes = ('sign', 'line', 'octaveChange')
+
     _DOC_ATTR: dict[str, str] = {
         'sign': '''
             The sign of the clef, generally, 'C', 'G', 'F', 'percussion', 'none' or None.
@@ -111,35 +135,6 @@ class Clef(base.Music21Object):
         # musicxml has an attribute for clefOctaveChange,
         # an integer to show transposing clef
 
-    def __eq__(self, other):
-        '''
-        two Clefs are equal if their class is the same, their sign is the same,
-        their line is the same and their octaveChange is the same.
-
-
-        >>> c1 = clef.PercussionClef()
-        >>> c2 = clef.NoClef()
-        >>> c1 == c2
-        False
-        >>> c3 = clef.TrebleClef()
-        >>> c4 = clef.TrebleClef()
-        >>> c3 == c4
-        True
-        >>> c4.octaveChange = -1
-        >>> c3 == c4
-        False
-        '''
-        try:
-            if (self.__class__ == other.__class__
-                    and self.sign == other.sign
-                    and self.line == other.line
-                    and self.octaveChange == other.octaveChange):
-                return True
-            else:
-                return False
-        except AttributeError:
-            return False
-
     def _reprInternal(self):
         return ''
 
diff --git a/music21/derivation.py b/music21/derivation.py
index 78182e564..887034169 100644
--- a/music21/derivation.py
+++ b/music21/derivation.py
@@ -304,6 +304,14 @@ class Derivation(SlottedObjectMixin):
             self._origin = origin
             # self._origin = common.wrapWeakref(origin)
 
+    @property
+    def originId(self) -> int | None:
+        '''
+        Return the Python id (=memory location) of the origin.
+        (Same as id(derivation.origin).  Not the same as derivation.origin.ind)
+        '''
+        return self._originId
+
     @property
     def rootDerivation(self) -> base.Music21Object | None:
         r'''
diff --git a/music21/duration.py b/music21/duration.py
index efd748351..4d44609f0 100644
--- a/music21/duration.py
+++ b/music21/duration.py
@@ -1199,6 +1199,7 @@ class Tuplet(prebase.ProtoM21Object):
                 return False
         return True
 
+
     # PRIVATE METHODS #
 
     def _reprInternal(self):
@@ -1745,7 +1746,6 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
         >>> aDur != bDur
         True
 
-
         >>> cDur == bDur
         True
 
@@ -3100,6 +3100,17 @@ class FrozenDuration(common.objects.FrozenObject, Duration):
     >>> fd.dots = 1
     Traceback (most recent call last):
     TypeError: This FrozenDuration instance is immutable.
+
+    FrozenDurations can be used as set/dict keys with stability.
+
+    >>> {fd}
+    {<music21.duration.FrozenDuration 3.5>}
+
+    Copying a FrozenDuration returns the original, so it is super fast.
+
+    >>> import copy
+    >>> copy.deepcopy(fd) is fd
+    True
     '''
     __slots__ = ()
 
diff --git a/music21/interval.py b/music21/interval.py
index 6b2c47959..346ec4ba8 100644
--- a/music21/interval.py
+++ b/music21/interval.py
@@ -3123,11 +3123,8 @@ class Interval(IntervalBase):
         >>> a == 'a4'
         False
         '''
-        if other is None:
+        if not super().__eq__(other):
             return False
-        elif not hasattr(other, 'diatonic') or not hasattr(other, 'chromatic'):
-            return False
-
         if (self.diatonic == other.diatonic
                 and self.chromatic == other.chromatic):
             return True
diff --git a/music21/key.py b/music21/key.py
index 5f8f2297e..d1a288fb0 100644
--- a/music21/key.py
+++ b/music21/key.py
@@ -331,10 +331,24 @@ class KeySignature(base.Music21Object):
     False
     >>> unusual.accidentalsApplyOnlyToOctave = True
 
+    Equality
+    --------
+    Two KeySignatures are equal if they pass all `super().__eq__` checks and
+    their sharps are equal.  Currently, non-standard key signatures are not
+    checked (this may change).
+
+    >>> sharp2 = key.KeySignature(2)
+    >>> flat4 = key.KeySignature(-4)
+    >>> sharp2 == flat4
+    False
+    >>> sharp2 == key.KeySignature(2)
+    True
+
     Changed in v.7 -- `sharps` defaults to 0 (key of no flats/sharps)
     rather than `None` for nontraditional keys.
     '''
     _styleClass = style.TextStyle
+    equalityAttributes = ('sharps',)
 
     # note that musicxml permits non-traditional keys by specifying
     # one or more altered tones; these are given as pairs of
@@ -382,18 +396,6 @@ class KeySignature(base.Music21Object):
             output = f'{abs(ns)} flats'
         return output
 
-    def __eq__(self, other):
-        '''
-        two KeySignatures are equal if their sharps are equal.
-        '''
-        try:
-            if self.sharps == other.sharps:
-                return True
-            else:
-                return False
-        except AttributeError:
-            return False
-
     def _reprInternal(self):
         return 'of ' + self._strDescription()
 
@@ -932,6 +934,21 @@ class Key(KeySignature, scale.DiatonicScale):
     <music21.key.Key of E major>
     >>> key.Key('F#m')
     <music21.key.Key of f# minor>
+
+    Equality
+    --------
+    Two Keys are equal if their tonics' names are equal, their sharps are equal,
+    and their modes are equal (and they pass all superclass tests)
+
+    >>> k = key.Key(pitch.Pitch('C4'))
+    >>> k2 = key.Key(pitch.Pitch('C5'))
+    >>> k == k2
+    True
+    >>> k.mode = 'minor'
+    >>> k == k2
+    False
+
+    Changed in v8: keys now compare equal regardless of the octave of their tonics.
     '''
     _sharps = 0
     _mode: str | None = None
@@ -999,18 +1016,9 @@ class Key(KeySignature, scale.DiatonicScale):
         return f'{tonic} {self.mode}'
 
     def __eq__(self, other):
-        '''
-        Two Keys are equal if their tonics are equal and their modes are equal.
-
-        Changed in v8: keys now compare equal regardless of the octave of their tonics:
-
-        >>> k = key.Key(pitch.Pitch('C4'))
-        >>> k2 = key.Key(pitch.Pitch('C5'))
-        >>> k == k2
-        True
-        '''
-        if not isinstance(other, Key):
-            return NotImplemented
+        if not KeySignature.__eq__(self, other):
+            return False
+        # don't check on Scale equality
         return self.tonic.name == other.tonic.name and self.mode == other.mode
 
     @property
diff --git a/music21/lily/translate.py b/music21/lily/translate.py
index 67de49ded..c7ec80a51 100644
--- a/music21/lily/translate.py
+++ b/music21/lily/translate.py
@@ -290,7 +290,7 @@ class LilypondConverter:
             if m21ObjectIn[variant.Variant]:
                 # has variants. so we need to make a deepcopy...
                 m21ObjectIn = variant.makeAllVariantsReplacements(m21ObjectIn, recurse=True)
-                m21ObjectIn.makeVariantBlocks()
+                variant.makeVariantBlocks(m21ObjectIn)
 
         if ('Stream' not in c) or ('Measure' in c) or ('Voice' in c):
             scoreObj = stream.Score()
diff --git a/music21/note.py b/music21/note.py
index b538a2370..bfd2e82e2 100644
--- a/music21/note.py
+++ b/music21/note.py
@@ -553,11 +553,18 @@ class GeneralNote(base.Music21Object):
     >>> gn = note.GeneralNote(type='16th', dots=2)
     >>> gn.quarterLength
     0.4375
+
+    Equality
+    --------
+    GeneralNote objects are equal if they pass superclass tests (e.g., their durations are equal),
+    and they have the same articulation and expression classes (in any order),
+    and their ties are equal.
     '''
     isNote = False
     isRest = False
     isChord = False
     _styleClass: type[style.Style] = style.NoteStyle
+    equalityAttributes: tuple[str, ...] = ('tie',)
 
     # define order for presenting names in documentation; use strings
     _DOC_ORDER = ['duration', 'quarterLength']
@@ -610,32 +617,28 @@ class GeneralNote(base.Music21Object):
         self._tie: tie.Tie | None = None  # store a Tie object
 
     def __eq__(self, other):
-        '''
-        General Note objects are equal if their durations are equal, and
-        they have the same articulation and expression classes (in any order),
-        and their ties are equal.
-        '''
-        if not isinstance(other, GeneralNote):
-            return NotImplemented
-        # checks type, dots, tuplets, quarterLength, uses Pitch.__eq__
-        if self.duration != other.duration:
+        if not super().__eq__(other):
             return False
+
+        # note: tie is in equalityClasses, so already checked above.
+
         # Articulations are a list of Articulation objects.
         # Converting them to Set objects produces ordered cols that remove duplicates.
         # However, we must then convert to list to match based on class ==
         # not on class id().
-        if (sorted({x.classes[0] for x in self.articulations})
-                != sorted({x.classes[0] for x in other.articulations})):
-            return False
-        if (sorted({x.classes[0] for x in self.expressions})
-                != sorted({x.classes[0] for x in other.expressions})):
-            return False
+        for search in 'articulations', 'expressions':
+            my_art_express_classes = [type(x) for x in getattr(self, search)]
+            other_art_express_classes = [type(x) for x in getattr(other, search)]
+            if len(my_art_express_classes) != len(other_art_express_classes):
+                return False
+            if set(my_art_express_classes) != set(other_art_express_classes):
+                return False
 
-        # Tie objects if present compare only type
-        if self.tie != other.tie:
-            return False
         return True
 
+    def __hash__(self):
+        return super().__hash__()
+
     # --------------------------------------------------------------------------
     @property
     def tie(self) -> tie.Tie | None:
@@ -982,6 +985,11 @@ class NotRest(GeneralNote):
             information about the beaming of this note.''',
     }
 
+    # Should volume be here too?  and _chordAttached?
+    equalityAttributes: tuple[str, ...] = (
+        'notehead', 'noteheadFill', 'noteheadParenthesis', 'beams'
+    )
+
     def __init__(self,
                  beams: beam.Beams | None = None,
                  **keywords):
@@ -1001,30 +1009,14 @@ class NotRest(GeneralNote):
     # ==============================================================================================
     # Special functions
     # ==============================================================================================
-    def __eq__(self, other):
-        if super().__eq__(other) is NotImplemented:
-            return NotImplemented
-        if not super().__eq__(other):
-            return False
-        if not isinstance(other, NotRest):
-            return False
-
-        if self.notehead != other.notehead:
-            return False
-        if self.noteheadFill != other.noteheadFill:
-            return False
-        if self.noteheadParenthesis != other.noteheadParenthesis:
-            return False
-        # Q: should volume need to be equal?
-        if self.beams != other.beams:
-            return False
-        return True
 
     def _deepcopySubclassable(self: _NotRestType,
                               memo: dict[int, t.Any] | None = None,
                               *,
                               ignoreAttributes: set[str] | None = None) -> _NotRestType:
         new = super()._deepcopySubclassable(memo, ignoreAttributes={'_chordAttached'})
+        if TYPE_CHECKING:
+            new = t.cast(_NotRestType, new)
         # let the chord restore _chordAttached
 
         # after copying, if a Volume exists, it is linked to the old object
@@ -1036,7 +1028,7 @@ class NotRest(GeneralNote):
     def __deepcopy__(self, memo=None):
         '''
         As NotRest objects have a Volume, objects, and Volume objects
-        store weak refs to the client object, need to specialize deep copy handling
+        store refs to the client object, need to specialize deepcopy handling
 
         >>> import copy
         >>> n = note.NotRest()
@@ -1044,6 +1036,8 @@ class NotRest(GeneralNote):
         >>> m = copy.deepcopy(n)
         >>> m.volume.client is m
         True
+        >>> n.volume.client is n
+        True
         '''
         # environLocal.printDebug(['calling NotRest.__deepcopy__', self])
         return self._deepcopySubclassable(memo=memo)
@@ -1389,12 +1383,10 @@ class Note(NotRest):
     for instance a C quarter-note and a D# eighth-tied-to-32nd are both
     a single Note object.
 
-
     A Note knows both its total duration and how to express itself as a set of
     tied notes of different lengths. For instance, a note of 2.5 quarters in
     length could be half tied to eighth or dotted quarter tied to quarter.
 
-
     The first argument to the Note is the pitch name (with or without
     octave, see the introduction to :class:`music21.pitch.Pitch`).
     Further arguments can be specified as keywords (such as type, dots, etc.)
@@ -1427,17 +1419,6 @@ class Note(NotRest):
     >>> note.Note(64).nameWithOctave
     'E4'
 
-    Two notes are considered equal if their most important attributes
-    (such as pitch, duration,
-    articulations, and ornaments) are equal.  Attributes
-    that might change based on the wider context
-    of a note (such as offset)
-    are not compared. This test does not look at lyrics in
-    establishing equality.  (It may in the future.)
-
-    >>> note.Note('C4') == note.Note('C4')
-    True
-
     All keyword args that are valid for Duration or Pitch objects
     are valid (as well as those for superclasses, NotRest, GeneralNote,
     Music21Object):
@@ -1447,8 +1428,71 @@ class Note(NotRest):
     'C#2'
     >>> n.duration
     <music21.duration.Duration 0.875>
+
+    Equality and ordering
+    ---------------------
+    Two notes are equal if they pass all the equality tests for NotRest and their
+    pitches are equal.
+
+    Attributes that might change based on the wider context
+    of a note (such as offset) are not compared. This test does not look at lyrics in
+    establishing equality.  (It may in the future.)
+
+    >>> note.Note('C4') == note.Note('C4')
+    True
+
+    Enharmonics are not equal:
+
+    >>> note.Note('D#4') == note.Note('E-4')
+    False
+
+    >>> note.Note('C4', type='half') == note.Note('C4', type='quarter')
+    False
+
+    Notes, like pitches, also have an ordering based on their pitches.
+
+    >>> highE = note.Note('E5')
+    >>> lowF = note.Note('F2')
+    >>> otherHighE = note.Note('E5')
+
+    >>> highE > lowF
+    True
+    >>> highE < lowF
+    False
+    >>> highE >= otherHighE
+    True
+    >>> highE <= otherHighE
+    True
+
+    Notice you cannot compare Notes w/ ints or anything that does not a have a
+    `.pitch` attribute.
+
+    >>> highE < 50
+    Traceback (most recent call last):
+    TypeError: '<' not supported between instances of 'Note' and 'int'
+
+    Note also that two objects can be >= and <= without being equal, because
+    only pitch-height is being compared in <, <=, >, >= but duration and other
+    elements are compared in equality.
+
+    >>> otherHighE.duration.type = 'whole'
+
+    Now otherHighE is != highE
+
+    >>> highE == otherHighE
+    False
+
+    But it is both >= and <= it:
+
+    >>> highE >= otherHighE
+    True
+    >>> highE <= otherHighE
+    True
+
+    (The pigeonhole principle police have a bounty out on my head for this.)
     '''
     isNote = True
+    equalityAttributes: tuple[str, ...] = ('pitch',)
 
     # Defines the order of presenting names in the documentation; use strings
     _DOC_ORDER = ['duration', 'quarterLength', 'nameWithOctave']
@@ -1492,72 +1536,8 @@ class Note(NotRest):
     def _reprInternal(self):
         return self.name
 
-    def __eq__(self, other):
-        '''
-        Tests Equality. See docs under Note above
-        (since __eq__'s docs don't display)
-
-        >>> n1 = note.Note()
-        >>> n1.pitch.name = 'G#'
-        >>> n2 = note.Note()
-        >>> n2.pitch.name = 'A-'
-        >>> n3 = note.Note()
-        >>> n3.pitch.name = 'G#'
-        >>> n1 == n2
-        False
-        >>> n1 == n3
-        True
-        >>> n3.duration.quarterLength = 3
-        >>> n1 == n3
-        False
-
-        >>> n1 == 5
-        False
-        '''
-        if not isinstance(other, type(self)):
-            return NotImplemented
-
-        # checks pitch.octave, pitch.accidental, uses Pitch.__eq__
-        if self.pitch != other.pitch:
-            return False
-
-        return super().__eq__(other)
 
     def __lt__(self, other):
-        '''
-        __lt__, __gt__, __le__, __ge__ all use a pitch comparison.
-
-        >>> highE = note.Note('E5')
-        >>> lowF = note.Note('F2')
-        >>> otherHighE = note.Note('E5')
-
-        >>> highE > lowF
-        True
-        >>> highE < lowF
-        False
-        >>> highE >= otherHighE
-        True
-        >>> highE <= otherHighE
-        True
-
-        Notice you cannot compare Notes w/ ints or anything not pitched.
-
-        >>> highE < 50
-        Traceback (most recent call last):
-        TypeError: '<' not supported between instances of 'Note' and 'int'
-
-        Note also that two objects can be >= and <= without being equal, because
-        only pitch-height is being compared in <, <=, >, >= but duration and other
-        elements are compared in equality.
-
-        >>> otherHighE.duration.type = 'whole'
-        >>> highE >= otherHighE
-        True
-        >>> highE <= otherHighE
-        True
-        >>> highE == otherHighE
-        False
-        '''
         try:
             return self.pitch < other.pitch
         except AttributeError:
@@ -1832,6 +1812,8 @@ class Unpitched(NotRest):
     AttributeError: 'Unpitched' object has no attribute 'pitch'
     '''
 
+    equalityAttributes = ('displayStep', 'displayOctave')
+
     def __init__(self,
                  displayName=None,
                  **keywords):
@@ -1845,19 +1827,6 @@ class Unpitched(NotRest):
             self.displayStep = display_pitch.step
             self.displayOctave = display_pitch.octave
 
-    def __eq__(self, other):
-        if super().__eq__(other) is NotImplemented:
-            return NotImplemented
-        if not super().__eq__(other):
-            return False
-        if not isinstance(other, Unpitched):
-            return False
-        if self.displayStep != other.displayStep:
-            return False
-        if self.displayOctave != other.displayOctave:
-            return False
-        return True
-
     def _getStoredInstrument(self):
         return self._storedInstrument
 
@@ -1935,6 +1904,24 @@ class Rest(GeneralNote):
     True
     >>> r3.duration.quarterLength
     2.0
+
+    Two rests are considered equal if their durations are equal.
+
+    >>> r1 = note.Rest('quarter')
+    >>> r2 = note.Rest('quarter')
+    >>> r1 == r2
+    True
+    >>> r1 != r2
+    False
+
+    >>> r2.duration.quarterLength = 4/3
+    >>> r1 == r2
+    False
+
+    A rest is never equal to a note.
+
+    >>> r1 == note.Note()
+    False
     '''
     isRest = True
     name = 'rest'
@@ -1991,29 +1978,6 @@ class Rest(GeneralNote):
             ql_string = str(ql)
             return f'{ql_string}ql'
 
-    def __eq__(self, other):
-        '''
-        A Music21 rest is equal to another object if that object is also a rest which
-        has the same duration.
-
-        >>> r1 = note.Rest()
-        >>> r2 = note.Rest()
-        >>> r1 == r2
-        True
-        >>> r1 != r2
-        False
-
-        >>> r2.duration.quarterLength = 4/3
-        >>> r1 == r2
-        False
-        >>> r1 == note.Note()
-        False
-        '''
-        if not isinstance(other, Rest):
-            return NotImplemented
-
-        return super().__eq__(other)
-
     @property
     def fullName(self) -> str:
         '''
diff --git a/music21/percussion.py b/music21/percussion.py
index 895eff1ca..9169f7aa8 100644
--- a/music21/percussion.py
+++ b/music21/percussion.py
@@ -48,14 +48,27 @@ class PercussionChord(chord.ChordBase):
     Traceback (most recent call last):
     TypeError: every element of notes must be a note.Note or note.Unpitched object
 
+    Equality
+    --------
+    Two PercussionChord objects are equal if their notes are equal *and in the same
+    order* (this is different from Chord, but necessary because we cannot compare
+    based just on pitch equality)
+
+    >>> pChord == pChord2
+    True
+    >>> pChord3 = percussion.PercussionChord([note.Unpitched('D4')])
+    >>> pChord == pChord3
+    False
+
     OMIT_FROM_DOCS
 
     See the repr of an empty percussion chord:
 
     >>> percussion.PercussionChord()
     <music21.percussion.PercussionChord object at 0x...>
-    '''
 
+    This is in OMIT
+    '''
     isChord = False
 
     def __deepcopy__(self, memo=None):
@@ -64,6 +77,18 @@ class PercussionChord(chord.ChordBase):
             n._chordAttached = new
         return new
 
+    def __eq__(self, other):
+        '''
+        Returns True if all the notes are equal and in the same order.
+        '''
+        if not super().__eq__(other):
+            return False
+        # super ensures that both have same number of notes.
+        for my_n, other_n in zip(self.notes, other.notes):
+            if my_n != other_n:
+                return False
+        return True
+
     @property
     def notes(self) -> tuple[note.NotRest, ...]:
         return tuple(self._notes)
diff --git a/music21/roman.py b/music21/roman.py
index fa18d129f..953e7589a 100644
--- a/music21/roman.py
+++ b/music21/roman.py
@@ -1744,6 +1744,7 @@ class RomanNumeral(harmony.Harmony):
     ()
 
     Equality:
+    ---------
 
     Two RomanNumerals compare equal if their `NotRest` components
     (noteheads, beams, expressions, articulations, etc.) are equal
@@ -1771,15 +1772,27 @@ class RomanNumeral(harmony.Harmony):
     >>> rn2 == rn4
     True
 
-    Changed in v6.5 -- caseMatters is keyword only. It along with sixthMinor and
-    seventhMinor are now the only allowable keywords to pass in.
+    >>> rn5 = roman.RomanNumeral('bII6', 'c')
+    >>> rn6 = roman.RomanNumeral('bII6', 'c')
+    >>> rn5 == rn6
+    True
+    >>> rn7 = roman.RomanNumeral('N6', 'c')
+    >>> rn5 == rn7
+    False
+
+
+    * Changed in v6.5 -- caseMatters is keyword only. It along with sixthMinor and
+      seventhMinor are now the only allowable keywords to pass in.
 
-    Changed in v7 -- RomanNumeral.romanNumeral will always give a "b" for a flattened
-    degree (i.e., '-II' becomes 'bII') as this is what people expect in looking at
-    the figure.
+    * Changed in v7 -- RomanNumeral.romanNumeral will always give a "b" for a flattened
+      degree (i.e., '-II' becomes 'bII') as this is what people expect in looking at
+      the figure.
+
+    * Changed in v.7.3 -- figures that are not normally used to indicate inversion
+      such as V54 (a suspension) no longer give strange inversions.
 
-    Changed in v8 -- Figures are now validated as alphanumeric or containing one of
-    the following symbols (after the example "V"):
+    * Changed in v8 -- Figures are now validated as alphanumeric or containing one of
+      the following symbols (after the example "V"):
 
     >>> specialCharacterFigure = roman.RomanNumeral('V#+-/[]')
     >>> specialCharacterFigure
@@ -1797,9 +1810,6 @@ class RomanNumeral(harmony.Harmony):
     Traceback (most recent call last):
     music21.roman.RomanNumeralException: Invalid figure: V64==53
 
-    * Changed in v.7.3 -- figures that are not normally used to indicate inversion
-      such as V54 (a suspension) no longer give strange inversions.
-
     OMIT_FROM_DOCS
 
     Things that were giving us trouble:
@@ -1861,8 +1871,10 @@ class RomanNumeral(harmony.Harmony):
 
     (NOTE: all this is omitted -- look at OMIT_FROM_DOCS above)
     '''
-    # TODO: document better! what is inherited and what is new?
+    equalityAttributes = ('figure', 'key')
+
 
+    # TODO: document better! what is inherited and what is new?
     _alterationRegex = re.compile(r'^(b+|-+|#+)')
     _omittedStepsRegex = re.compile(r'(\[(no[1-9]+)+]\s*)+')
     _addedStepsRegex = re.compile(r'\[add(b*|-*|#*)(\d+)+]\s*')
@@ -2310,7 +2322,14 @@ class RomanNumeral(harmony.Harmony):
         self._functionalityScore: int | None = None
         self.followsKeyChange: bool = False
 
-    # SPECIAL METHODS #
+    def __eq__(self, other):
+        '''
+        Compare equality, just based on NotRest and on figure and key
+        '''
+        # NotRest == will be used, but the equality attributes of RomanNumeral
+        # will be picked up as well.
+        return note.NotRest.__eq__(self, other)
+
 
     def _reprInternal(self):
         if hasattr(self.key, 'tonic'):
@@ -2318,22 +2337,6 @@ class RomanNumeral(harmony.Harmony):
         else:
             return self.figure
 
-    def __eq__(self, other) -> bool:
-        '''
-        Compare equality, just based on NotRest and on figure and key
-        '''
-        if not isinstance(other, RomanNumeral):
-            return NotImplemented
-        if note.NotRest.__eq__(self, other) is NotImplemented:
-            return NotImplemented
-        if not note.NotRest.__eq__(self, other):
-            return False
-        if self.key != other.key:
-            return False
-        if self.figure != other.figure:
-            return False
-        return True
-
     # PRIVATE METHODS #
     def _parseFigure(self):
         '''
diff --git a/music21/scale/intervalNetwork.py b/music21/scale/intervalNetwork.py
index cadb7689b..b8ad58e6b 100644
--- a/music21/scale/intervalNetwork.py
+++ b/music21/scale/intervalNetwork.py
@@ -493,8 +493,13 @@ class IntervalNetwork:
         '''
         # compare all nodes and edges; if the same, and all keys are the same,
         # then matched
-        return (isinstance(other, self.__class__)
-                and self.__dict__ == other.__dict__)
+        if not isinstance(other, self.__class__):
+            return False
+        for attr in ('edgeIdCount', 'nodeIdCount', 'edges', 'nodes',
+                     'octaveDuplicating', 'deterministic', 'pitchSimplification'):
+            if getattr(self, attr) != getattr(other, attr):
+                return False
+        return True
 
     def fillBiDirectedEdges(self, edgeList: Sequence[interval.Interval | str]):
         # noinspection PyShadowingNames
diff --git a/music21/spanner.py b/music21/spanner.py
index 7da3046c7..c577c7577 100644
--- a/music21/spanner.py
+++ b/music21/spanner.py
@@ -203,6 +203,8 @@ class Spanner(base.Music21Object):
     >>> sp1.completeStatus = True
     '''
 
+    equalityAttributes = ('spannerStorage',)
+
     def __init__(self,
                  *spannedElements: t.Union[base.Music21Object,
                                            Sequence[base.Music21Object]],
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 90dbaed08..9b574f988 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -21,7 +21,7 @@ and :class:`~music21.stream.Score` objects, are also in this module.
 '''
 from __future__ import annotations
 
-from collections import namedtuple, OrderedDict
+from collections import deque, namedtuple, OrderedDict
 from collections.abc import Collection, Iterable, Sequence
 import copy
 from fractions import Fraction
@@ -378,6 +378,15 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
 
         self.coreElementsChanged()
 
+    def __eq__(self, other):
+        '''
+        No two streams are ever equal unless they are the same Stream
+        '''
+        return self is other
+
+    def __hash__(self) -> int:
+        return id(self) >> 4
+
     def _reprInternal(self) -> str:
         if self.id is not None:
             if self.id != id(self) and str(self.id) != str(id(self)):
@@ -1838,21 +1847,35 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
             #     pass  # This should maybe just call a function renumberMeasures
         self.coreElementsChanged(clearIsSorted=False)
 
-    def pop(self, index: int) -> base.Music21Object:
+    def pop(self, index: int | None = None) -> base.Music21Object:
         '''
         Return and remove the object found at the
         user-specified index value. Index values are
         those found in `elements` and are not necessary offset order.
 
         >>> a = stream.Stream()
-        >>> a.repeatInsert(note.Note('C'), list(range(10)))
-        >>> junk = a.pop(0)
+        >>> for i in range(12):  # notes C, C#, etc. to B
+        ...     a.append(note.Note(i))
+        >>> a.pop(0)
+        <music21.note.Note C>
         >>> len(a)
-        9
+        11
+
+        If nothing is given, then it pops the last thing from the stream.
+
+        >>> a.pop()
+        <music21.note.Note B>
+        >>> len(a)
+        10
         '''
         eLen = len(self._elements)
         # if less than base length, it is in _elements
-        if index < eLen:
+        if index is None:
+            if self._endElements:
+                post = self._endElements.pop()
+            else:
+                post = self._elements.pop()
+        elif index < eLen:
             post = self._elements.pop(index)
         else:  # it is in the _endElements
             post = self._endElements.pop(index - eLen)
@@ -11611,6 +11634,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         ...    v1measure.insert(e.offset, e)
 
         >>> v2 = variant.Variant()
+        >>> v2.replacementDuration = 4.0
         >>> v2measure1 = stream.Measure()
         >>> v2measure2 = stream.Measure()
         >>> v2.insert(0.0, v2measure1)
@@ -11621,13 +11645,12 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         ...    v2measure2.insert(e.offset, e)
 
         >>> v3 = variant.Variant()
-        >>> v2.replacementDuration = 4.0
         >>> v3.replacementDuration = 4.0
         >>> v1.groups = ['docVariants']
         >>> v2.groups = ['docVariants']
         >>> v3.groups = ['docVariants']
 
-        >>> s.insert(4.0, v1)    # replacement variant
+        >>> s.insert(4.0, v1)   # replacement variant
         >>> s.insert(12.0, v2)  # insertion variant (2 bars replace 1 bar)
         >>> s.insert(20.0, v3)  # deletion variant (0 bars replace 1 bar)
         >>> s.show('text')
@@ -11797,9 +11820,8 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         # Loop through all variants, deal with replacement variants and
         # save insertion and deletion for later.
         for v in returnObj.getElementsByClass(variant.Variant):
-            if group is not None:
-                if group not in v.groups:
-                    continue  # skip those that are not part of this group
+            if group is not None and group not in v.groups:
+                continue  # skip those that are not part of this group
 
             lengthType = v.lengthType
 
@@ -11874,7 +11896,6 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         Helper function for activateVariants. Activates variants which are the same size there the
         region they replace.
 
-
         >>> v = variant.Variant()
         >>> variantDataM1 = [('b', 'eighth'), ('c', 'eighth'),
         ...                  ('a', 'quarter'), ('a', 'quarter'),('b', 'quarter')]
@@ -11973,7 +11994,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         # matching by span means that we remove all elements with the
         # span defined by the variant
         else:
-            deletedMeasures = []
+            deletedMeasures = deque()
             insertedMeasures = []
             highestNumber = None
 
@@ -11987,17 +12008,17 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
                 # environLocal.printDebug(
                 #     ['matchBySpan', matchBySpan, 'activateVariants', 'removing', e])
                 self.remove(e)
-                if getattr(e, 'isMeasure', False):
+                if isinstance(e, Measure):
                     # Save deleted measure numbers.
                     deletedMeasures.append(e.number)
 
             for e in v.elements:
                 oInStream = vStart + e.getOffsetBySite(v.containedSite)
                 self.insert(oInStream, e)
-                if getattr(e, 'isMeasure', False):
+                if isinstance(e, Measure):
                     if deletedMeasures:  # If there measure numbers left to use, use them.
                         # Assign the next highest deleted measure number
-                        e.number = deletedMeasures.pop(False)
+                        e.number = deletedMeasures.popleft()
                         # Save the highest number used so far (for use in the case
                         # that there are extra measures with no numbers at the end)
                         highestNumber = e.number
@@ -12014,7 +12035,8 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
 
             # If deletedMeasures != [], then there were more deleted measures than
             # inserted and the remaining numbers in deletedMeasures are those that were removed.
-            return deletedMeasures, (highestNumber, insertedMeasures)
+            return (list(deletedMeasures),
+                    (highestNumber, insertedMeasures))
             # In the case that the variant and stream are in the same time-signature,
             # this should return []
 
@@ -12095,7 +12117,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         '''
         from music21 import variant
 
-        deletedMeasures = []  # For keeping track of what measure numbers are deleted
+        deletedMeasures = deque()  # For keeping track of what measure numbers are deleted
         # length of the deleted region
         lengthDifference = v.replacementDuration - v.containedHighestTime
 
@@ -12110,7 +12132,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
 
         # this will always remove elements before inserting
         for e in targets:
-            if getattr(e, 'isMeasure', False):  # if a measure is deleted, save its number
+            if isinstance(e, Measure):  # if a measure is deleted, save its number
                 deletedMeasures.append(e.number)
             oInVariant = self.elementOffset(e) - vStart
             removed.insert(oInVariant, e)
@@ -12120,11 +12142,11 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         highestNumber = None
         insertedMeasures = []
         for e in v.elements:
-            if getattr(e, 'isMeasure', False):
+            if isinstance(e, Measure):
                 # If there are deleted numbers still saved, assign this measure the
                 # next highest and remove it from the list.
                 if deletedMeasures:
-                    e.number = deletedMeasures.pop(False)
+                    e.number = deletedMeasures.popleft()
                     # Save the highest number assigned so far. If there are numberless
                     # inserted measures at the end, this will name where to begin numbering.
                     highestNumber = e.number
@@ -12144,8 +12166,11 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
 
         # each variant leaves a gap, this saves the required information about those gaps
         # In most cases, inserted measures should be [].
-        return (deletionStart, lengthDifference, []), deletedMeasures, (
-            highestNumber, insertedMeasures)
+        return (
+            (deletionStart, lengthDifference, []),
+            list(deletedMeasures),
+            (highestNumber, insertedMeasures)
+        )
 
     def _insertInsertionVariant(self, v, matchBySpan=True):
         # noinspection PyShadowingNames
@@ -12229,7 +12254,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         '''
         from music21 import variant
 
-        deletedMeasures = []
+        deletedMeasures = deque()
         removed = variant.Variant()  # what group should this have?
         removed.groups = ['default']  # for now, default
         removed.replacementDuration = v.containedHighestTime
@@ -12240,7 +12265,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
 
         # this will always remove elements before inserting
         for e in targets:
-            if getattr(e, 'isMeasure', False):  # Save deleted measure numbers.
+            if isinstance(e, Measure):  # Save deleted measure numbers.
                 deletedMeasures.append(e.number)
             oInVariant = self.elementOffset(e) - vStart
             removed.insert(oInVariant, e)
@@ -12250,11 +12275,11 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         highestMeasure = None
         insertedMeasures = []
         for e in v.elements:
-            if getattr(e, 'isMeasure', False):
+            if isinstance(e, Measure):
                 # If there are deleted measure numbers left, assign the next
                 # inserted measure the next highest number and remove it.
                 if deletedMeasures:
-                    e.number = deletedMeasures.pop(False)
+                    e.number = deletedMeasures.popleft()
                     highestMeasure = e.number
                     # Save the highest number assigned so far,
                     # so we know where to begin numbering new measures.
@@ -12289,7 +12314,7 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         return (highestMeasure, insertedMeasures), deletedMeasures
 
     def _removeOrExpandGaps(self, listOffsetDurExemption,
-                            isRemove=True, inPlace=False, exemptClasses=None):
+                            isRemove=True, inPlace=False):
         '''
         Helper for activateVariants. Takes a list of tuples in the form
         (startOffset, duration, [list, of, exempt, objects]). If isRemove is True,
@@ -12299,7 +12324,6 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         itself should not be moved except by other gaps. This is poorly written
         and should be re-written, but it is difficult to describe.
 
-
         >>> s = stream.Stream()
         >>> s.insert(5.0, note.Note('a'))
         >>> s.insert(10.0, note.Note('b'))
@@ -12340,22 +12364,12 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         returnObjDuration = returnObj.duration.quarterLength
 
         # If any classes should be exempt from gap closing or expanding, this deals with those.
-        classList = []
-        if exemptClasses is None:
-            classList = None
-        else:
-            for e in returnObj.elements:
-                if type(e) not in classList:  # pylint: disable=unidiomatic-typecheck
-                    classList.append(type(e))
-            for c in exemptClasses:
-                if c in classList:
-                    classList.remove(c)
-
         if isRemove is True:
             shiftDur = 0.0
             listSorted = sorted(listOffsetDurExemption, key=lambda target: target[0])
             for i, durTuple in enumerate(listSorted):
                 startOffset, durationAmount, exemptObjects = durTuple
+                exemptObjectSet = set(id(e) for e in exemptObjects)  # use set id, not == checking
                 if i + 1 < len(listSorted):
                     endOffset = listSorted[i + 1][0]
                     includeEnd = False
@@ -12368,10 +12382,11 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
                                                        endOffset,
                                                        includeEndBoundary=includeEnd,
                                                        mustFinishInSpan=False,
-                                                       mustBeginInSpan=True,
-                                                       classList=classList):
+                                                       mustBeginInSpan=True):
 
-                    if e in exemptObjects:
+                    if id(e) in exemptObjectSet:
+                        continue
+                    if not inPlace and e.derivation.originId in exemptObjectSet:
                         continue
 
                     elementOffset = e.getOffsetBySite(returnObj)
@@ -12397,14 +12412,18 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
 
             for offset in sorted(shiftsDict, key=lambda off: -1 * off):
                 shiftDur, endOffset, includeEnd, exemptObjects, exemptShift = shiftsDict[offset]
+                # for speed and ID not == checking
+                exemptObjectSet = set(id(e) for e in exemptObjects)
                 for e in returnObj.getElementsByOffset(offset,
                                                        endOffset,
                                                        includeEndBoundary=includeEnd,
                                                        mustFinishInSpan=False,
-                                                       mustBeginInSpan=True,
-                                                       classList=classList):
+                                                       mustBeginInSpan=True):
 
-                    if e in exemptObjects:
+                    if (
+                        id(e) in exemptObjectSet
+                        or (not inPlace and e.derivation.originId in exemptObjectSet)
+                    ):
                         elementOffset = e.getOffsetBySite(returnObj)
                         returnObj.coreSetElementOffset(e, elementOffset + exemptShift)
                         continue
@@ -12697,42 +12716,6 @@ class Stream(core.StreamCore, t.Generic[M21ObjType]):
         else:
             return returnObj
 
-    def makeVariantBlocks(self):
-        '''
-        from music21 import *
-        '''
-        from music21 import variant
-        variantsToBeDone = self.getElementsByClass(variant.Variant)
-
-        for v in variantsToBeDone:
-            startOffset = self.elementOffset(v)
-            endOffset = v.replacementDuration + startOffset
-            conflictingVariants = self.getElementsByOffset(offsetStart=startOffset,
-                                                           offsetEnd=endOffset,
-                                                           includeEndBoundary=False,
-                                                           mustFinishInSpan=False,
-                                                           mustBeginInSpan=True,
-                                                           classList=['Variant'])
-            for cV in conflictingVariants:
-                oldReplacementDuration = cV.replacementDuration
-                if self.elementOffset(cV) == startOffset:
-                    continue  # do nothing
-                else:
-                    shiftOffset = self.elementOffset(cV) - startOffset
-                    r = note.Rest()
-                    r.duration.quarterLength = shiftOffset
-                    r.style.hideObjectOnPrint = True
-                    for el in cV._stream:
-                        oldOffset = el.getOffsetBySite(cV._stream)
-                        cV._stream.coreSetElementOffset(el, oldOffset + shiftOffset)
-                    cV.coreElementsChanged()
-                    cV.insert(0.0, r)
-                    cV.replacementDuration = oldReplacementDuration
-                    self.remove(cV)
-                    self.insert(startOffset, cV)
-                    variantsToBeDone.append(cV)
-
-
 # -----------------------------------------------------------------------------
 
 
diff --git a/music21/tree/spans.py b/music21/tree/spans.py
index 0e6dc8405..ed8d29807 100644
--- a/music21/tree/spans.py
+++ b/music21/tree/spans.py
@@ -345,10 +345,7 @@ class ElementTimespan(Timespan):
 
     # SPECIAL METHODS #
     def __eq__(self, other):
-        if self is other:
-            return True
-        else:
-            return False
+        return self is other
 
     def __repr__(self):
         typeName = type(self).__name__
diff --git a/music21/variant.py b/music21/variant.py
index 87252823c..f0f3254f5 100644
--- a/music21/variant.py
+++ b/music21/variant.py
@@ -2498,6 +2498,40 @@ def _getPreviousElement(s, v):
     return returnElement
 
 
+def makeVariantBlocks(s):
+    '''
+    Unknown and undocumented.  Used only in lily/translate -- for musicdiff.
+    '''
+    from music21 import variant
+    variantsToBeDone = s.getElementsByClass(variant.Variant)
+
+    for v in variantsToBeDone:
+        startOffset = s.elementOffset(v)
+        endOffset = v.replacementDuration + startOffset
+        conflictingVariants = s.getElementsByOffset(offsetStart=startOffset,
+                                                    offsetEnd=endOffset,
+                                                    includeEndBoundary=False,
+                                                    mustFinishInSpan=False,
+                                                    mustBeginInSpan=True,
+                                                    classList=[variant.Variant])
+        for cV in conflictingVariants:
+            oldReplacementDuration = cV.replacementDuration
+            if s.elementOffset(cV) == startOffset:
+                continue  # do nothing
+            else:
+                shiftOffset = s.elementOffset(cV) - startOffset
+                r = note.Rest()
+                r.duration.quarterLength = shiftOffset
+                r.style.hideObjectOnPrint = True
+                for el in cV._stream:
+                    oldOffset = el.getOffsetBySite(cV._stream)
+                    cV._stream.coreSetElementOffset(el, oldOffset + shiftOffset)
+                cV.coreElementsChanged()
+                cV.insert(0.0, r)
+                cV.replacementDuration = oldReplacementDuration
+                s.remove(cV)
+                s.insert(startOffset, cV)
+                variantsToBeDone.append(cV)
 
 
 # ------------------------------------------------------------------------------


Test patch:
diff --git a/music21/meter/tests.py b/music21/meter/tests.py
index 901fccce0..b599d693f 100644
--- a/music21/meter/tests.py
+++ b/music21/meter/tests.py
@@ -86,15 +86,24 @@ class Test(unittest.TestCase):
         a[3] = a[3].subdivide(4)
         self.assertEqual(str(a), '{{1/8+1/8}+1/4+1/4+{1/16+1/16+1/16+1/16}}')
 
-    def testMeterDeepcopy(self):
+    def testMeterSequenceDeepcopy(self):
         a = MeterSequence()
         a.load('4/4', 4)
         b = copy.deepcopy(a)
-        self.assertNotEqual(a, b)
+        self.assertIsNot(a, b)
+        # TODO: this is work in progress.
+        # self.assertEqual(a, b)
 
+    def testTimeSignatureDeepcopy(self):
         c = TimeSignature('4/4')
         d = copy.deepcopy(c)
-        self.assertNotEqual(c, d)
+        self.assertIsNot(c, d)
+        self.assertEqual(c, d)
+
+        # TODO: this is work in progress
+        # e = TimeSignature('slow 6/8')
+        # f = TimeSignature('fast 6/8')
+        # self.assertNotEqual(e, f)
 
     def testGetBeams(self):
         ts = TimeSignature('6/8')
diff --git a/music21/test/test_base.py b/music21/test/test_base.py
index efe4c8b83..68427f26f 100644
--- a/music21/test/test_base.py
+++ b/music21/test/test_base.py
@@ -100,7 +100,8 @@ class Test(unittest.TestCase):
         a = Music21Object()
         a.id = 'test'
         b = copy.deepcopy(a)
-        self.assertNotEqual(a, b)
+        self.assertIsNot(a, b)
+        self.assertEqual(a, b)
         self.assertEqual(b.id, 'test')
 
     def testM21BaseSites(self):