Repository: cuthbertLab/music21
Pull request #944 (2021-04-02 03:28:25)

Safety on bool iterator, tempAttribute
Long forgotten cleanup commit.

no one expects this to fail:

```
n = note.Note()
s1 = stream.Stream([n])
s2 = stream.Stream([n])
assert n.activeSite is s2
assert s1.notes
assert n.activeSite is s2
```

but it did before.

Rule of thumb: we only set activeSite when the caller of the function has actually seen the element.

Add some useful contextmanagers in common/classTools

Remove another Py2.7 fallback.

Remove Eclipse/PyDev hints that were false alarms -- hopefully long-dead errors have been fixed.  Left in ones that would still be errors otherwise.


Patch:
diff --git a/documentation/docbuild/writers.py b/documentation/docbuild/writers.py
index 9642380e0..091675965 100644
--- a/documentation/docbuild/writers.py
+++ b/documentation/docbuild/writers.py
@@ -229,7 +229,7 @@ class IPythonNotebookReSTWriter(ReSTWriter):
     This class wraps the 3rd-party ``nbconvert`` Python script.
     '''
     def __init__(self):
-        from .iterators import IPythonNotebookIterator  # @UnresolvedImport
+        from .iterators import IPythonNotebookIterator
         super(IPythonNotebookReSTWriter, self).__init__()
         self.ipythonNotebookFilePaths = list(IPythonNotebookIterator())
         # Do not run self.setupOutputDirectory()
@@ -450,7 +450,7 @@ class IPythonNotebookReSTWriter(ReSTWriter):
             str(self.sourceToAutogenerated(ipythonNotebookFilePath))
         )[0]
 
-        app = nb.NbConvertApp.instance()  # @UndefinedVariable
+        app = nb.NbConvertApp.instance()
         app.initialize(argv=['--to', 'rst', '--output', outputPath,
                              str(ipythonNotebookFilePath)])
         app.writer.build_directory = str(ipythonNotebookFilePath.parent)
diff --git a/music21/analysis/discrete.py b/music21/analysis/discrete.py
index fe16ed5e1..630ef6709 100644
--- a/music21/analysis/discrete.py
+++ b/music21/analysis/discrete.py
@@ -1537,7 +1537,7 @@ class Test(unittest.TestCase):
         from music21 import converter
         from music21.musicxml import testFiles
         # use a musicxml test file with independently confirmed results
-        s = converter.parse(testFiles.edgefield82b)  # @UndefinedVariable
+        s = converter.parse(testFiles.edgefield82b)
 
         p = KrumhanslSchmuckler()
         k = p.getSolution(s)
diff --git a/music21/articulations.py b/music21/articulations.py
index fa46714b4..e47f5990e 100644
--- a/music21/articulations.py
+++ b/music21/articulations.py
@@ -83,6 +83,7 @@ import unittest
 
 from music21 import base
 from music21 import common
+from music21.common.classTools import tempAttribute
 from music21 import exceptions21
 from music21 import environment
 from music21 import style
@@ -295,9 +296,8 @@ class Spiccato(Staccato, Accent):
     '''
     def __init__(self):
         Staccato.__init__(self)
-        storedLengthShift = self.lengthShift
-        Accent.__init__(self)  # order matters...
-        self.lengthShift = storedLengthShift
+        with tempAttribute(self, 'lengthShift'):
+            Accent.__init__(self)  # order matters...
 
 
 class Tenuto(LengthArticulation):
diff --git a/music21/common/classTools.py b/music21/common/classTools.py
index fe99e5c69..7ef739618 100644
--- a/music21/common/classTools.py
+++ b/music21/common/classTools.py
@@ -9,10 +9,14 @@
 # Copyright:    Copyright © 2009-2015 Michael Scott Cuthbert and the music21 Project
 # License:      BSD, see license.txt
 # ------------------------------------------------------------------------------
-from typing import Any, Type
+import contextlib
+from typing import Any, Type, Dict
 
 # from music21 import exceptions21
-__all__ = ['isNum', 'isListLike', 'isIterable', 'classToClassStr', 'getClassSet']
+__all__ = [
+    'isNum', 'isListLike', 'isIterable', 'classToClassStr', 'getClassSet',
+    'tempAttribute', 'saveAttributes',
+]
 
 
 def isNum(usrData: Any) -> bool:
@@ -166,6 +170,67 @@ def getClassSet(instance, classNameTuple=None):
     return classSet
 
 
+TEMP_ATTRIBUTE_SENTINEL = object()
+
+
+@contextlib.contextmanager
+def tempAttribute(obj, attribute: str, new_val=TEMP_ATTRIBUTE_SENTINEL):
+    '''
+    Temporarily set an attribute in an object to another value
+    and then restore it afterwards.
+
+    >>> p = pitch.Pitch('C4')
+    >>> p.midi
+    60
+    >>> with common.classTools.tempAttribute(p, 'nameWithOctave', 'D#5'):
+    ...     p.midi
+    75
+    >>> p.nameWithOctave
+    'C4'
+
+    Setting to a new value is optional.
+
+    For working with multiple attributes see :func:`~music21.classTools.saveAttributes`.
+
+    New in v7.
+    '''
+    tempStorage = getattr(obj, attribute)
+    if new_val is not TEMP_ATTRIBUTE_SENTINEL:
+        setattr(obj, attribute, new_val)
+    try:
+        yield
+    finally:
+        setattr(obj, attribute, tempStorage)
+
+@contextlib.contextmanager
+def saveAttributes(obj, *attributeList):
+    '''
+    Save a number of attributes in an object and then restore them afterwards.
+
+    >>> p = pitch.Pitch('C#2')
+    >>> with common.classTools.saveAttributes(p, 'name', 'accidental'):
+    ...     p.step = 'E'
+    ...     p.accidental = pitch.Accidental('flat')
+    ...     p.nameWithOctave
+    'E-2'
+    >>> p.nameWithOctave
+    'C#2'
+
+    For storing and setting a value on a single attribute see
+    :func:`~music21.classTools.tempAttribute`.
+
+    New in v7.
+    '''
+    tempStorage: Dict[str, Any] = {}
+    for attribute in attributeList:
+        tempStorage[attribute] = getattr(obj, attribute)
+    try:
+        yield
+    finally:
+        for k, v in tempStorage.items():  # dicts are ordered in 3.7
+            setattr(obj, k, v)
+
+
 # ------------------------------------------------------------------------------
 # define presented order in documentation
 # _DOC_ORDER = [fromRoman, toRoman]
diff --git a/music21/common/decorators.py b/music21/common/decorators.py
index 55c19ecfa..a9aafece5 100644
--- a/music21/common/decorators.py
+++ b/music21/common/decorators.py
@@ -175,5 +175,5 @@ def cacheMethod(method):
 
 
 if __name__ == '__main__':
-    import music21  # @Reimport
+    import music21
     music21.mainTest()
diff --git a/music21/common/enums.py b/music21/common/enums.py
index a1fb9ce00..84f51afa4 100644
--- a/music21/common/enums.py
+++ b/music21/common/enums.py
@@ -132,6 +132,8 @@ class OffsetSpecial(StrEnum):
     False
     >>> 'lowestOffset' in OffsetSpecial
     True
+    >>> str(OffsetSpecial.AT_END)
+    'highestTime'
 
     New in v7.
     '''
diff --git a/music21/common/fileTools.py b/music21/common/fileTools.py
index 6beb56d52..4c5e2cba9 100644
--- a/music21/common/fileTools.py
+++ b/music21/common/fileTools.py
@@ -43,10 +43,7 @@ def cd(targetDir):
 
     will switch temporarily, and then switch back when leaving.
     '''
-    try:
-        cwd = os.getcwdu()  # unicode # @UndefinedVariable
-    except AttributeError:
-        cwd = os.getcwd()  # non unicode
+    cwd = os.getcwd()
 
     try:
         os.chdir(targetDir)
diff --git a/music21/common/parallel.py b/music21/common/parallel.py
index 1579f1186..69a01847b 100644
--- a/music21/common/parallel.py
+++ b/music21/common/parallel.py
@@ -106,7 +106,7 @@ def runParallel(iterable, parallelFunction, *,
     # pylint: disable=not-callable
     numCpus = cpus()
 
-    if numCpus == 1 or multiprocessing.current_process().daemon:  # @UndefinedVariable
+    if numCpus == 1 or multiprocessing.current_process().daemon:
         return runNonParallel(iterable, parallelFunction,
                               updateFunction=updateFunction,
                               updateMultiply=updateMultiply,
@@ -214,7 +214,7 @@ def cpus():
     '''
     Returns the number of CPUs or if >= 3, one less (to leave something out for multiprocessing)
     '''
-    cpuCount = multiprocessing.cpu_count()  # @UndefinedVariable
+    cpuCount = multiprocessing.cpu_count()
     if cpuCount >= 3:
         return cpuCount - 1
     else:
@@ -250,7 +250,7 @@ class Test(unittest.TestCase):
     def x_figure_out_segfault_testMultiprocess(self):
         files = ['bach/bwv66.6', 'schoenberg/opus19', 'AcaciaReel']
         # for importing into testSingleCoreAll we need the full path to the modules
-        from music21.common.parallel import _countN, _countUnpacked  # @UnresolvedImport
+        from music21.common.parallel import _countN, _countUnpacked
         output = runParallel(files, _countN)
         self.assertEqual(output, [165, 50, 131])
         runParallel(files, _countN,
diff --git a/music21/common/stringTools.py b/music21/common/stringTools.py
index 0b64e91c4..c44e5e13c 100644
--- a/music21/common/stringTools.py
+++ b/music21/common/stringTools.py
@@ -352,5 +352,5 @@ def removePunctuation(s: str) -> str:
 # -----------------------------------------------------------------------------
 
 if __name__ == '__main__':
-    import music21  # @Reimport
+    import music21
     music21.mainTest()
diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py
index 1df7dab69..4864660ed 100644
--- a/music21/converter/__init__.py
+++ b/music21/converter/__init__.py
@@ -1301,14 +1301,14 @@ class TestExternal(unittest.TestCase):  # pragma: no cover
 
     def testMusicXMLConversion(self):
         from music21.musicxml import testFiles
-        for mxString in testFiles.ALL:  # @UndefinedVariable
+        for mxString in testFiles.ALL:
             a = subConverters.ConverterMusicXML()
             a.parseData(mxString)
 
     def testMusicXMLTabConversion(self):
         from music21.musicxml import testFiles
 
-        mxString = testFiles.ALL[5]  # @UndefinedVariable
+        mxString = testFiles.ALL[5]
         a = subConverters.ConverterMusicXML()
         a.parseData(mxString)
 
@@ -1569,12 +1569,12 @@ class Test(unittest.TestCase):
     def testConversionMXMetadata(self):
         from music21.musicxml import testFiles
 
-        a = parse(testFiles.mozartTrioK581Excerpt)  # @UndefinedVariable
+        a = parse(testFiles.mozartTrioK581Excerpt)
         self.assertEqual(a.metadata.composer, 'Wolfgang Amadeus Mozart')
         self.assertEqual(a.metadata.title, 'Quintet for Clarinet and Strings')
         self.assertEqual(a.metadata.movementName, 'Menuetto (Excerpt from Second Trio)')
 
-        a = parse(testFiles.binchoisMagnificat)  # @UndefinedVariable
+        a = parse(testFiles.binchoisMagnificat)
         self.assertEqual(a.metadata.composer, 'Gilles Binchois')
         # this gets the best title available, even though this is movement title
         self.assertEqual(a.metadata.title, 'Excerpt from Magnificat secundi toni')
diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py
index 8560f7c97..d5d719c2b 100644
--- a/music21/converter/subConverters.py
+++ b/music21/converter/subConverters.py
@@ -318,7 +318,7 @@ class ConverterIPython(SubConverter):
     #     '''
     #     import random
     #     from music21.vexflow import toMusic21j
-    #     from IPython.display import HTML # @UnresolvedImport
+    #     from IPython.display import HTML
     #     vfp = toMusic21j.VexflowPickler()
     #     vfp.mode = 'jsonSplit'
     #     outputCode = vfp.fromObject(s)
@@ -1141,7 +1141,7 @@ class ConverterRomanText(SubConverter):
     def parseData(self, strData, number=None):
         from music21.romanText import rtObjects
         from music21.romanText import translate as romanTextTranslate
-        rtf = rtObjects.RTFile()  # not sure why -- @UndefinedVariable
+        rtf = rtObjects.RTFile()
         rtHandler = rtf.readstr(strData)
         if rtHandler.definesMovements():
             # this re-defines Score as an Opus
diff --git a/music21/corpus/corpora.py b/music21/corpus/corpora.py
index c237fc515..f1e13d409 100644
--- a/music21/corpus/corpora.py
+++ b/music21/corpus/corpora.py
@@ -946,7 +946,7 @@ class LocalCorpus(Corpus):
 #         className = getattr(virtual, corpusName)
 #         if callable(className):
 #             obj = className()
-#             if isinstance(obj, virtual.VirtualWork):  # @UndefinedVariable
+#             if isinstance(obj, virtual.VirtualWork):
 #                 if obj.corpusPath is not None:
 #                     _virtualWorks.append(obj)
 #     del corpusName
diff --git a/music21/features/__init__.py b/music21/features/__init__.py
index 23df684af..74e46372d 100644
--- a/music21/features/__init__.py
+++ b/music21/features/__init__.py
@@ -20,5 +20,5 @@ from music21.features import jSymbolic
 from music21.features import native
 
 # pylint: disable=redefined-builtin
-__doc__ = base.__doc__  # @ReservedAssignment @UndefinedVariable
+__doc__ = base.__doc__
 
diff --git a/music21/features/base.py b/music21/features/base.py
index c438495e8..d94913202 100644
--- a/music21/features/base.py
+++ b/music21/features/base.py
@@ -538,7 +538,6 @@ class DataInstance:
     multiple commonly-used stream representations once, providing rapid processing.
     '''
     # pylint: disable=redefined-builtin
-
     def __init__(self, streamOrPath=None, id=None):  # @ReservedAssignment
         if isinstance(streamOrPath, stream.Stream):
             self.stream = streamOrPath
@@ -897,7 +896,6 @@ class DataSet:
             self.addData(d, cv, thisId)
 
     # pylint: disable=redefined-builtin
-
     def addData(self, dataOrStreamOrPath, classValue=None, id=None):  # @ReservedAssignment
         '''
         Add a Stream, DataInstance, MetadataEntry, or path (Posix or str)
@@ -1814,7 +1812,7 @@ class Test(unittest.TestCase):
         from music21 import features
 
         # Need explicit import for pickling within the testSingleCoreAll context
-        from music21.features.base import _pickleFunctionNumPitches  # @UnresolvedImport
+        from music21.features.base import _pickleFunctionNumPitches
         import textwrap
 
         self.maxDiff = None
diff --git a/music21/features/native.py b/music21/features/native.py
index 8b44b9f8b..9f83bcdee 100644
--- a/music21/features/native.py
+++ b/music21/features/native.py
@@ -17,7 +17,7 @@ import math
 from typing import Optional
 
 from urllib.request import Request, urlopen
-from urllib.parse import urlencode  # @UnresolvedImport @Reimport
+from urllib.parse import urlencode
 
 
 from music21.features import base as featuresModule
diff --git a/music21/graph/plot.py b/music21/graph/plot.py
index 0e9f028ae..3ed0ad8e1 100644
--- a/music21/graph/plot.py
+++ b/music21/graph/plot.py
@@ -1543,7 +1543,8 @@ class TestExternal(unittest.TestCase):  # pragma: no cover
             (ScatterPitchClassQuarterLength, None, None),
             (ScatterPitchClassOffset, None, None),
             (ScatterPitchSpaceDynamicSymbol,
-             corpus.getWork('schumann/opus41no1', 2), 'Schumann Opus 41 No 1'),
+             corpus.getWork('schumann/opus41no1', 2),
+             'Schumann Opus 41 No 1'),
 
             # offset based horizontal
             (HorizontalBarPitchSpaceOffset, None, None),
@@ -1552,12 +1553,14 @@ class TestExternal(unittest.TestCase):  # pragma: no cover
             (ScatterWeightedPitchSpaceQuarterLength, None, None),
             (ScatterWeightedPitchClassQuarterLength, None, None),
             (ScatterWeightedPitchSpaceDynamicSymbol,
-             corpus.getWork('schumann/opus41no1', 2), 'Schumann Opus 41 No 1'),
+             corpus.getWork('schumann/opus41no1', 2),
+             'Schumann Opus 41 No 1'),
 
 
             # 3d graphs
             (Plot3DBarsPitchSpaceQuarterLength,
-             testFiles.mozartTrioK581Excerpt, 'Mozart Trio K581 Excerpt'),  # @UndefinedVariable
+             testFiles.mozartTrioK581Excerpt,
+             'Mozart Trio K581 Excerpt'),
 
             (WindowedKey, corpus.getWork('bach/bwv66.6.xml'), 'Bach BWV 66.6'),
             (WindowedAmbitus, corpus.getWork('bach/bwv66.6.xml'), 'Bach BWV 66.6'),
diff --git a/music21/graph/primitives.py b/music21/graph/primitives.py
index 526448ad9..4fc84ad00 100644
--- a/music21/graph/primitives.py
+++ b/music21/graph/primitives.py
@@ -1785,7 +1785,7 @@ class TestExternal(unittest.TestCase):  # pragma: no cover
         g.process()
 
     def testGraphNetworkxGraph(self):
-        extm = getExtendedModules()  # @UnusedVariable
+        extm = getExtendedModules()
 
         if extm.networkx is not None:  # pragma: no cover
             b = GraphNetworkxGraph(doneAction=None)
diff --git a/music21/graph/utilities.py b/music21/graph/utilities.py
index aa90bdc7c..fec3c8c80 100644
--- a/music21/graph/utilities.py
+++ b/music21/graph/utilities.py
@@ -43,12 +43,12 @@ def getExtendedModules():
     if 'matplotlib' in _missingImport:
         raise GraphException(
             'could not find matplotlib, graphing is not allowed')  # pragma: no cover
-    import matplotlib  # @UnresolvedImport
+    import matplotlib
     # backend can be configured from config file, matplotlibrc,
     # but an early test broke all processing
     # matplotlib.use('WXAgg')
     try:
-        from mpl_toolkits.mplot3d import Axes3D  # @UnresolvedImport
+        from mpl_toolkits.mplot3d import Axes3D
     except ImportError:  # pragma: no cover
         Axes3D = None
         environLocal.warn(
@@ -56,13 +56,14 @@ def getExtendedModules():
             + 'old version of six.py (< 1.9.0) on your system somewhere'
         )
 
-    from matplotlib import collections  # @UnresolvedImport
-    from matplotlib import patches  # @UnresolvedImport
+    from matplotlib import collections
+    from matplotlib import patches
 
     # from matplotlib.colors import colorConverter
-    import matplotlib.pyplot as plt  # @UnresolvedImport
+    import matplotlib.pyplot as plt
 
     try:
+        # noinspection PyPackageRequirements
         import networkx
     except ImportError:  # pragma: no cover
         networkx = None  # use for testing
diff --git a/music21/humdrum/instruments.py b/music21/humdrum/instruments.py
index f968fc86b..ed25d8703 100644
--- a/music21/humdrum/instruments.py
+++ b/music21/humdrum/instruments.py
@@ -264,14 +264,14 @@ def fromHumdrumInstrument(hdInst):
 class Test(unittest.TestCase):
 
     def testClasses(self):
-        from music21 import instrument  # @UnusedImport  # wrong -- is used
+        from music21 import instrument
 
         for x in humdrumInstrumentClassToInstrument:
             i = humdrumInstrumentClassToInstrument[x]
             self.assertNotEqual(getattr(instrument, i)().instrumentName, None)
 
     def testIndividuals(self):
-        from music21 import instrument  # @UnusedImport  # wrong -- is used
+        from music21 import instrument
 
         for x in humdrumInstruments:
             i = humdrumInstruments[x]
diff --git a/music21/ipython21/__init__.py b/music21/ipython21/__init__.py
index f18dfceaa..bc9cb54c3 100644
--- a/music21/ipython21/__init__.py
+++ b/music21/ipython21/__init__.py
@@ -36,8 +36,8 @@ def loadNoMagic():
 
 
 # if we are imported in an IPython environment, then load magic after half a second
-if common.runningUnderIPython():  # @UndefinedVariable
+if common.runningUnderIPython():
     from threading import Timer
     t = Timer(2, loadNoMagic)
     t.start()
-    # ipython21.load_no_magic() # @UndefinedVariable
+    # ipython21.load_no_magic()
diff --git a/music21/ipython21/ipExtension.py b/music21/ipython21/ipExtension.py
index 6e791a4b6..69b11a618 100644
--- a/music21/ipython21/ipExtension.py
+++ b/music21/ipython21/ipExtension.py
@@ -11,9 +11,9 @@ def load_ipython_extension(ip):
 
     MAY 2017: everything happens in converter.subConverters
     '''
-#     pngFormatter = ip.display_formatter.formatters['image/png']
-#     pngFormatter.for_type(music21.ipython21.objects.IPythonPNGObject,
-#                           music21.ipython21.objects.IPythonPNGObject.getData)
+    # pngFormatter = ip.display_formatter.formatters['image/png']
+    # pngFormatter.for_type(music21.ipython21.objects.IPythonPNGObject,
+    #                       music21.ipython21.objects.IPythonPNGObject.getData)
     try:
         from matplotlib import pyplot as plt
         plt.ion()
diff --git a/music21/lily/lilyObjects.py b/music21/lily/lilyObjects.py
index a909c240a..b5793941e 100644
--- a/music21/lily/lilyObjects.py
+++ b/music21/lily/lilyObjects.py
@@ -1283,7 +1283,6 @@ class LyPrefixCompositeMusic(LyObject):
                        | re_rhythmed_music
     '''
     # pylint: disable=redefined-builtin
-
     def __init__(self, type=None, genericPrefixMusicScm=None,  # @ReservedAssignment
                  simpleString=None, optionalId=None, optionalContextMod=None,
                  music=None, fraction=None, repeatedMusic=None,
diff --git a/music21/lily/translate.py b/music21/lily/translate.py
index 951430159..a7e4744c9 100644
--- a/music21/lily/translate.py
+++ b/music21/lily/translate.py
@@ -2494,7 +2494,7 @@ class LilypondConverter:
             from PIL import Image, ImageOps
             # noinspection PyBroadException
             try:
-                lilyImage = Image.open(str(lilyFile))  # @UndefinedVariable
+                lilyImage = Image.open(str(lilyFile))
                 lilyImage2 = ImageOps.expand(lilyImage, 10, 'white')
                 lilyImage2.save(str(lilyFile))
             except Exception:  # pylint: disable=broad-except
diff --git a/music21/metadata/caching.py b/music21/metadata/caching.py
index 859b45494..86fd7339d 100644
--- a/music21/metadata/caching.py
+++ b/music21/metadata/caching.py
@@ -327,7 +327,7 @@ class JobProcessor:
         jobs is a list of :class:`~music21.metadata.MetadataCachingJob` objects.
 
         '''
-        processCount = processCount or common.cpus()  # @UndefinedVariable
+        processCount = processCount or common.cpus()
         if processCount < 1:
             processCount = 1
         remainingJobs = len(jobs)
@@ -337,8 +337,8 @@ class JobProcessor:
         environLocal.printDebug(
             f'Processing {remainingJobs} jobs in parallel, with {processCount} processes.')
         results = []
-        job_queue = multiprocessing.JoinableQueue()  # @UndefinedVariable
-        result_queue = multiprocessing.Queue()  # @UndefinedVariable
+        job_queue = multiprocessing.JoinableQueue()
+        result_queue = multiprocessing.Queue()
         workers = [WorkerProcess(job_queue, result_queue)
                    for _ in range(processCount)]
         for worker in workers:
diff --git a/music21/midi/__init__.py b/music21/midi/__init__.py
index 32fdce403..70e0e2b64 100644
--- a/music21/midi/__init__.py
+++ b/music21/midi/__init__.py
@@ -38,7 +38,7 @@ import os
 import string
 import struct
 import sys
-import unicodedata  # @UnresolvedImport
+import unicodedata
 import unittest
 from typing import Optional, Union, Tuple
 
diff --git a/music21/roman.py b/music21/roman.py
index d7259e7ed..18f054c99 100644
--- a/music21/roman.py
+++ b/music21/roman.py
@@ -1415,7 +1415,7 @@ class RomanNumeral(harmony.Harmony):
     'half-diminished seventh chord'
 
     >>> alteredChordHalfDim3rdInv.romanNumeral
-    '-ii'
+    'bii'
 
     >>> alteredChordHalfDim3rdInv.romanNumeralAlone
     'ii'
@@ -1605,6 +1605,41 @@ class RomanNumeral(harmony.Harmony):
     >>> r.pitches
     ()
 
+    Equality:
+
+    Two RomanNumerals compare equal if their `NotRest` components
+    (noteheads, beams, expressions, articulations, etc.) are equal
+    and if their figures and keys are equal:
+
+    >>> c1 = chord.Chord('C4 E4 G4 C5')
+    >>> c2 = chord.Chord('C3 E4 G4')
+    >>> rn1 = roman.romanNumeralFromChord(c1, 'C')
+    >>> rn2 = roman.romanNumeralFromChord(c2, 'C')
+    >>> rn1 == rn2
+    True
+    >>> rn1.duration.type = 'half'
+    >>> rn1 == rn2
+    False
+    >>> rn3 = roman.RomanNumeral('I', 'd')
+    >>> rn2 == rn3
+    False
+    >>> rn3.key = key.Key('C')
+    >>> rn2 == rn3
+    True
+    >>> rn4 = roman.RomanNumeral('ii', 'C')
+    >>> rn2 == rn4
+    False
+    >>> rn4.figure = 'I'
+    >>> 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.
+
+    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.
+
 
     OMIT_FROM_DOCS
 
@@ -1649,37 +1684,7 @@ class RomanNumeral(harmony.Harmony):
     >>> cp(r)
     ['G4']
 
-
-    Equality:
-
-    Two RomanNumerals compare equal if their `NotRest` components
-    (noteheads, beams, expressions, articulations, etc.) are equal
-    and if their figures and keys are equal:
-
-    >>> c1 = chord.Chord('C4 E4 G4 C5')
-    >>> c2 = chord.Chord('C3 E4 G4')
-    >>> rn1 = roman.romanNumeralFromChord(c1, 'C')
-    >>> rn2 = roman.romanNumeralFromChord(c2, 'C')
-    >>> rn1 == rn2
-    True
-    >>> rn1.duration.type = 'half'
-    >>> rn1 == rn2
-    False
-    >>> rn3 = roman.RomanNumeral('I', 'd')
-    >>> rn2 == rn3
-    False
-    >>> rn3.key = key.Key('C')
-    >>> rn2 == rn3
-    True
-    >>> rn4 = roman.RomanNumeral('ii', 'C')
-    >>> rn2 == rn4
-    False
-    >>> rn4.figure = 'I'
-    >>> 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.
+    (NOTE: all this is omitted -- look at OMIT_FROM_DOCS above)
     '''
     # TODO: document better! what is inherited and what is new?
 
@@ -2923,19 +2928,32 @@ class RomanNumeral(harmony.Harmony):
     def romanNumeral(self):
         '''
         Read-only property that returns either the romanNumeralAlone (e.g. just
-        II) or the frontAlterationAccidental.modifier + romanNumeralAlone (e.g. #II)
+        II) or the frontAlterationAccidental.modifier (with 'b' for '-') + romanNumeralAlone
+        (e.g. #II, bII)
 
         >>> from music21 import roman
         >>> rn = roman.RomanNumeral('#II7')
         >>> rn.romanNumeral
         '#II'
 
+        >>> rn = roman.RomanNumeral('Ger+6')
+        >>> rn.romanNumeral
+        'Ger'
+
+        >>> rn = roman.RomanNumeral('bbII/V')
+        >>> rn.romanNumeral
+        'bbII'
+        >>> rn = roman.RomanNumeral('--II/V')
+        >>> rn.romanNumeral
+        'bbII'
         '''
+        if self.romanNumeralAlone in ('Ger', 'Sw', 'It', 'Fr'):
+            return self.romanNumeralAlone
         if self.frontAlterationAccidental is None:
             return self.romanNumeralAlone
-        else:
-            return (self.frontAlterationAccidental.modifier
-                    + self.romanNumeralAlone)
+
+        return (self.frontAlterationAccidental.modifier.replace('-', 'b')
+                + self.romanNumeralAlone)
 
     @property
     def figure(self):
diff --git a/music21/scale/intervalNetwork.py b/music21/scale/intervalNetwork.py
index 9d52dbdfb..8d3778ee5 100644
--- a/music21/scale/intervalNetwork.py
+++ b/music21/scale/intervalNetwork.py
@@ -33,12 +33,6 @@ import copy
 import unittest
 
 from collections import OrderedDict
-try:
-    import networkx
-except ImportError:
-    # lacking this does nothing
-    networkx = None
-    # _missingImport.append('networkx')
 
 from music21 import exceptions21
 from music21 import interval
@@ -2189,6 +2183,8 @@ class IntervalNetwork:
         of this IntervalNetwork if networkx is installed
 
         '''
+        # noinspection PyPackageRequirements
+        import networkx  # pylint: disable=import-error
         weight = 1
         style = 'solid'
 
@@ -2244,14 +2240,14 @@ class IntervalNetwork:
 
         Requires networkx to be installed.
         '''
-#
-#         >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE
-#         >>> s.plot('pianoroll', doneAction=None) #_DOCS_HIDE
-#         >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8')
-#         >>> #_DOCS_SHOW s.plot('pianoroll')
-
-#         .. image:: images/PlotHorizontalBarPitchSpaceOffset.*
-#             :width: 600
+        #
+        # >>> s = corpus.parse('bach/bwv324.xml') #_DOCS_HIDE
+        # >>> s.plot('pianoroll', doneAction=None) #_DOCS_HIDE
+        # >>> #_DOCS_SHOW s = corpus.parse('bach/bwv57.8')
+        # >>> #_DOCS_SHOW s.plot('pianoroll')
+        #
+        # .. image:: images/PlotHorizontalBarPitchSpaceOffset.*
+        #     :width: 600
         if pitchObj is None:
             pitchObj = pitch.Pitch('C4')
 
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 58eb08383..4d8f92316 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -3090,8 +3090,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
     # --------------------------------------------------------------------------
     # methods that act on individual elements without requiring
-    # @ coreElementsChanged to fire
-
+    # coreElementsChanged to fire
     def addGroupForElements(self, group, classFilter=None, *, recurse=False):
         '''
         Add the group to the groups attribute of all elements.
diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py
index 26cdbf877..9df634b46 100644
--- a/music21/stream/iterator.py
+++ b/music21/stream/iterator.py
@@ -20,6 +20,7 @@ import unittest
 import warnings
 
 from music21 import common
+from music21.common.classTools import tempAttribute, saveAttributes
 from music21.common.enums import OffsetSpecial
 from music21.exceptions21 import StreamException
 from music21.stream import filters
@@ -111,7 +112,7 @@ class StreamIterator(prebase.ProtoM21Object):
         self.iterSection = '_elements'
 
         self.cleanupOnStop = False
-        self.restoreActiveSites = restoreActiveSites
+        self.restoreActiveSites: bool = restoreActiveSites
 
         self.overrideDerivation = None
 
@@ -365,7 +366,7 @@ class StreamIterator(prebase.ProtoM21Object):
         '''
         if self._len is not None:
             return self._len
-        self._len = len(self.matchingElements())
+        self._len = len(self.matchingElements(restoreActiveSites=False))
         self.reset()
         return self._len
 
@@ -415,8 +416,12 @@ class StreamIterator(prebase.ProtoM21Object):
         '''
         if self._len is not None:
             return bool(self._len)
-        for unused in self:
-            return True
+
+        # do not change active site of first element in bool
+        with tempAttribute(self, 'restoreActiveSites', False):
+            for unused in self:
+                return True
+
         return False
 
     def clone(self: _SIter) -> _SIter:
@@ -572,7 +577,7 @@ class StreamIterator(prebase.ProtoM21Object):
     # ---------------------------------------------------------------
     # getting items
 
-    def matchingElements(self):
+    def matchingElements(self, *, restoreActiveSites: bool = True):
         '''
         returns a list of elements that match the filter.
 
@@ -601,7 +606,8 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> sI_notes
         <music21.stream.iterator.StreamIterator for Part:tn3/4 @:0>
 
-        Note that this used to be True until v6.0.3
+        Adding a filter to the Stream iterator returns a new Stream iterator; it
+        does not change the original.
 
         >>> sI_notes is sI
         False
@@ -616,21 +622,24 @@ class StreamIterator(prebase.ProtoM21Object):
         [<music21.note.Note C>, <music21.note.Note D>,
          <music21.note.Note E>, <music21.note.Note F>, <music21.note.Note G>,
          <music21.note.Note A>]
+
+        If restoreActiveSties is False then the elements will not have
+        their activeSites changed (callers should use it when they do not plan to actually
+        expose the elements to users, such as in `__len__`).
+
+        Added in v7. -- restoreActiveSites
         '''
         if self._matchingElements is not None:
             return self._matchingElements
 
-        savedIndex = self.index
-        savedRestoreActiveSites = self.restoreActiveSites
-        self.restoreActiveSites = True
-
-        me = [x for x in self]  # pylint: disable=unnecessary-comprehension
-
-        self.reset()
+        with saveAttributes(self, 'restoreActiveSites', 'index'):
+            self.restoreActiveSites = restoreActiveSites
+            me = [x for x in self]  # pylint: disable=unnecessary-comprehension
+            self.reset()
 
-        self.index = savedIndex
-        self.restoreActiveSites = savedRestoreActiveSites
-        self._matchingElements = me
+        if restoreActiveSites == self.restoreActiveSites:
+            # cache, if we are using the iterator default.
+            self._matchingElements = me
 
         return me
 
@@ -1604,12 +1613,11 @@ class RecursiveIterator(StreamIterator):
         self.activeInformation['lastYielded'] = None
         super().reset()
 
-    def matchingElements(self):
+    def matchingElements(self, *, restoreActiveSites=True):
         # saved parent iterator later?
         # will this work in mid-iteration? Test, or do not expose till then.
-        savedRecursiveIterator = self.childRecursiveIterator
-        fe = super().matchingElements()
-        self.childRecursiveIterator = savedRecursiveIterator
+        with tempAttribute(self, 'childRecursiveIterator'):
+            fe = super().matchingElements(restoreActiveSites=restoreActiveSites)
         return fe
 
     def iteratorStack(self):
diff --git a/setup.py b/setup.py
index 185507f19..3c92cb1dc 100644
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@ with open(path, 'r') as f:
     lines = f.read()
     exec(lines)
 
-m21version = __version__ # @UndefinedVariable
+m21version = __version__
 
 DESCRIPTION = 'A Toolkit for Computer-Aided Musical Analysis.'
 DESCRIPTION_LONG = """A Toolkit for Computer-Aided Musical Analysis.


Test patch:
diff --git a/music21/metadata/testMetadata.py b/music21/metadata/testMetadata.py
index e8df5cb43..f7dcb4617 100644
--- a/music21/metadata/testMetadata.py
+++ b/music21/metadata/testMetadata.py
@@ -12,7 +12,7 @@ class Test(unittest.TestCase):
         from music21 import converter
         from music21.musicxml import testFiles as mTF
 
-        c = converter.parse(mTF.mozartTrioK581Excerpt)  # @UndefinedVariable
+        c = converter.parse(mTF.mozartTrioK581Excerpt)
         md = c.metadata
 
         self.assertEqual(md.movementNumber, '3')
@@ -23,7 +23,7 @@ class Test(unittest.TestCase):
         # get contributors directly from Metadata interface
         self.assertEqual(md.composer, 'Wolfgang Amadeus Mozart')
 
-        c = converter.parse(mTF.binchoisMagnificat)  # @UndefinedVariable
+        c = converter.parse(mTF.binchoisMagnificat)
         md = c.metadata
         self.assertEqual(md.composer, 'Gilles Binchois')
 
@@ -44,7 +44,7 @@ class Test(unittest.TestCase):
         self.assertEqual(md.title, 'Concerto in F')
 
         # test getting meta data from an imported source
-        c = converter.parse(mTF.mozartTrioK581Excerpt)  # @UndefinedVariable
+        c = converter.parse(mTF.mozartTrioK581Excerpt)
         md = c.metadata
 
         self.assertEqual(md.movementNumber, '3')
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index a6a0d1a05..679cc8007 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -6092,7 +6092,7 @@ class Test(unittest.TestCase):
         from music21.musicxml import testFiles
         from music21 import converter
         # testing a file a file with dynamics
-        a = converter.parse(testFiles.schumannOp48No1)  # @UndefinedVariable
+        a = converter.parse(testFiles.schumannOp48No1)
         unused_b = a.flat
         # b= a.flat.extendDuration(dynamics.Dynamic)
 
@@ -8105,6 +8105,13 @@ class Test(unittest.TestCase):
         os.remove(out)
         os.remove(otherFile)
 
+    def testActiveSiteAfterBoolIteration(self):
+        n = note.Note()
+        s1 = Stream([n])
+        s2 = Stream([n])
+        self.assertIs(n.activeSite, s2)
+        self.assertTrue(s1.notes)
+        self.assertIs(n.activeSite, s2)
 
 # -----------------------------------------------------------------------------
 
diff --git a/music21/test/commonTest.py b/music21/test/commonTest.py
index a9242a5b3..82288d01e 100644
--- a/music21/test/commonTest.py
+++ b/music21/test/commonTest.py
@@ -271,7 +271,7 @@ class ModuleGather:
         for dirPath, unused_dirNames, filenames in os.walk(self.dirParent):
             self._visitFunc(None, dirPath, filenames)
 
-        if common.cpus() > 4:  # @UndefinedVariable
+        if common.cpus() > 4:
             self.modulePaths.sort(key=manyCoreSortFunc)
         else:
             self.modulePaths.sort()
diff --git a/music21/test/multiprocessTest.py b/music21/test/multiprocessTest.py
index 060e7f34a..8a54e800a 100644
--- a/music21/test/multiprocessTest.py
+++ b/music21/test/multiprocessTest.py
@@ -191,7 +191,7 @@ def mainPoolRunner(testGroup=('test',), restoreEnvironmentDefaults=False, leaveO
             timeouts = 0
             eventsProcessed += 1
             summaryOutput.append(newResult)
-        except multiprocessing.TimeoutError:  # @UndefinedVariable
+        except multiprocessing.TimeoutError:
             timeouts += 1
             if timeouts == 5 and eventsProcessed > 0:
                 print('Delay in processing, seconds: ', end='')