Repository: cuthbertLab/music21
Pull request #682 (2020-11-28 21:51:41)

Sane Defaults for platform='nix'
Changes needed to run unittests in music21/test 
out-of-the-box on Ubuntu 20.04

* set default environment when platform='nix'

* fix test case for testMultipartMeasures() per TODO

"this should show instruments, this is presently not showing instruments  probably b/c when appending to s Stream activeSite is set to that stream"

![image](https://user-images.githubusercontent.com/26659886/100526608-83259380-3198-11eb-9ca0-91afe56c522b.png)


coveralls: 
[![Coverage Status](https://coveralls.io/builds/35301997/badge)](https://coveralls.io/builds/35301997)

Coverage remained the same at 90.969% when pulling **05dcfcc7470f36b6afd5d43b084153dc4c728585 on wilsonify:master** into **37fc2fece6e9aeeed9af622e5c17cf97285a283a on cuthbertLab:master**.


Patch:
diff --git a/.github/scripts/install_ubuntu_deps.sh b/.github/scripts/install_ubuntu_deps.sh
new file mode 100755
index 000000000..a62ac93a9
--- /dev/null
+++ b/.github/scripts/install_ubuntu_deps.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+set -e  # error if anything returns non-zero exit code
+
+# needed for some tests that add Desktop
+mkdir ~/Desktop
+
+sudo apt-get install -y libpng-dev
+sudo apt-get install -y python-qt4
+wget -q http://lilypond.org/downloads/binaries/linux-64/lilypond-2.19.83-1.linux-64.sh
+sh lilypond-2.19.83-1.linux-64.sh --batch
+export PATH=/home/runner/bin:$PATH
+pip3 install -r requirements.txt
+pip3 install coveralls
+pip3 install scipy
+pip3 install python-Levenshtein
+pip3 install setuptools
+pip3 install coverage
+cd music21
+python -m compileall music21
diff --git a/.github/workflows/maincheck.yml b/.github/workflows/maincheck.yml
new file mode 100644
index 000000000..b9d1c29e2
--- /dev/null
+++ b/.github/workflows/maincheck.yml
@@ -0,0 +1,33 @@
+name: maincheck
+
+on:
+    push:
+        branches:
+        - master
+        - '*CI*'
+    pull_request:
+        branches:
+        - '*'
+
+jobs:
+  run_tests:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: [3.6, 3.7, 3.8, 3.9]
+    steps:
+    - uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+    - uses: actions/checkout@v2
+    - name: Install Ubuntu dependencies
+      run: ./.github/scripts/install_ubuntu_deps.sh
+    - name: Setup Lilypond
+      run: python -c 'from music21 import environment; environment.UserSettings()["lilypondPath"] = "/home/runner/bin/lilypond"'
+    - name: Run Main Test script
+      run: python -c 'from music21.test.testSingleCoreAll import ciMain as ci; ci()'
+    - name: Coveralls
+      if: ${{ matrix.python-version == '3.7' }}
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      run: coveralls
diff --git a/.github/workflows/pythonpylint.yml b/.github/workflows/pythonpylint.yml
index c3a0bf25a..d9a958d84 100644
--- a/.github/workflows/pythonpylint.yml
+++ b/.github/workflows/pythonpylint.yml
@@ -8,14 +8,14 @@ on:
   pull_request:
     branches:
       - '*'
-        
+
 jobs:
   lint:
     runs-on: ubuntu-latest
     strategy:
       max-parallel: 4
       matrix:
-        python-version: [3.7]
+        python-version: [3.8]
 
     steps:
     - uses: actions/checkout@v1
@@ -38,7 +38,7 @@ jobs:
     strategy:
       max-parallel: 4
       matrix:
-        python-version: [3.7]
+        python-version: [3.8]
 
     steps:
     - uses: actions/checkout@v1
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
deleted file mode 100644
index 057bd7acc..000000000
--- a/.scrutinizer.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-filter:
-    excluded_paths:
-        - music21/test/*
-        - music21/ext/*
-
-tools:
-    pylint:
-        python_version: 3     # Possible values: 2, 3
-        config_file: '.pylintrc'
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 37878ed73..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-language: python
-
-matrix:
-    include:
-        - python: 3.6
-          dist: xenial
-          sudo: false
-        - python: 3.7
-          dist: xenial
-          sudo: true
-        - python: 3.8
-          dist: xenial
-          sudo: true
-
-services:
-  - xvfb
-
-before_install:
-    # needed for some tests that add Desktop
-    - mkdir ~/Desktop
-    - sudo apt-get install -y libpng-dev
-    - sudo apt-get install -y python-qt4
-    - export PYTHONPATH=/home/travis/cuthbertLab/music21base
-    - wget -q http://lilypond.org/downloads/binaries/linux-64/lilypond-2.19.83-1.linux-64.sh
-    - pip3 install coveralls
-    - sh lilypond-2.19.83-1.linux-64.sh --batch
-    - export PATH=/home/travis/bin:$PATH
-
-install:
-    - pip install -r requirements.txt
-    - pip install scipy
-    - pip install python-Levenshtein
-    - pip install setuptools
-    - pip install coverage
-    - cd ..
-    - mv music21 music21base
-    - cd music21base
-    - python -m compileall music21
-
-script:
-    - python -c 'from music21.test.testSingleCoreAll import travisMain as tm; tm()'
-
-after_success:
-    - if [ "$TRAVIS_PYTHON_VERSION" == "3.6" ]; then coveralls; fi
diff --git a/music21/base.py b/music21/base.py
index b504a18d1..9f3047fae 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -2406,7 +2406,7 @@ class Music21Object(prebase.ProtoM21Object):
             offset = foundOffset
             atEnd = 0
 
-        if self.duration is not None and self.duration.isGrace:
+        if self.duration.isGrace:
             isNotGrace = 0
         else:
             isNotGrace = 1
@@ -2835,9 +2835,6 @@ class Music21Object(prebase.ProtoM21Object):
         from music21 import tie
         quarterLength = opFrac(quarterLength)
 
-        if self.duration is None:  # pragma: no cover
-            raise Exception('cannot split an element that has a Duration of None')
-
         if quarterLength > self.duration.quarterLength:
             raise duration.DurationException(
                 f'cannot split a duration ({self.duration.quarterLength}) '
@@ -2993,10 +2990,6 @@ class Music21Object(prebase.ProtoM21Object):
         >>> [n.quarterLength for n in post]
         [1.0, 1.0, 1.0]
         '''
-        if self.duration is None:  # pragma: no cover
-            raise Music21ObjectException(
-                'cannot split an element that has a Duration of None')
-
         if opFrac(sum(quarterLengthList)) != self.duration.quarterLength:
             raise Music21ObjectException(
                 'cannot split by quarter length list whose sum is not '
diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py
index 9a44b9024..748bc1b19 100644
--- a/music21/chord/__init__.py
+++ b/music21/chord/__init__.py
@@ -242,9 +242,8 @@ class Chord(note.NotRest):
         #     for thisNote in notes:
         #         # get duration from first note
         #         # but should other notes have the same duration?
-        #         if hasattr(thisNote, 'duration') and thisNote.duration is not None:
-        #             self.duration = notes[0].duration
-        #             break
+        #         self.duration = notes[0].duration
+        #         break
 
         if 'beams' in keywords:
             self.beams = keywords['beams']
@@ -4069,13 +4068,100 @@ class Chord(note.NotRest):
         >>> dyad2.commonName
         'Diminished Fourth'
 
+        Compound intervals are given in full if there are only two distinct pitches:
+
+        >>> dyad1 = chord.Chord('C4 E5')
+        >>> dyad1.commonName
+        'Major Tenth'
+
+        But if there are more pitches, then the interval is given in a simpler form:
+
+        >>> dyad1 = chord.Chord('C4 C5 E5 C6')
+        >>> dyad1.commonName
+        'Major Third with octave doublings'
+
+
+        If there are multiple enharmonics present just the simple
+        number of semitones is returned.
+
+        >>> dyad1 = chord.Chord('C4 E5 F-5 B#7')
+        >>> dyad1.commonName
+        '4 semitones'
+
+
+
+        Special handling of one-pitchClass chords:
+
+        >>> gAlone = chord.Chord(['G4'])
+        >>> gAlone.commonName
+        'note'
+        >>> gAlone = chord.Chord('G4 G4')
+        >>> gAlone.commonName
+        'unison'
+        >>> gAlone = chord.Chord('G4 G5')
+        >>> gAlone.commonName
+        'Perfect Octave'
+        >>> gAlone = chord.Chord('G4 G6')
+        >>> gAlone.commonName
+        'Perfect Double-octave'
+        >>> gAlone = chord.Chord('G4 G5 G6')
+        >>> gAlone.commonName
+        'multiple octaves'
+        >>> gAlone = chord.Chord('F#4 G-4')
+        >>> gAlone.commonName
+        'enharmonic unison'
+
+
         Changed in v5.5: special cases for checking enharmonics in some cases
+        Changed in v6.5: better handling of 1- and 2-pitchClass chords.
         '''
         cta = self.chordTablesAddress
+        if cta.cardinality == 1:
+            if len(self.pitches) == 1:
+                return 'note'
+            pitchNames = {p.name for p in self.pitches}
+            pitchPSes = {p.ps for p in self.pitches}
+            if len(pitchNames) == 1:
+                if len(pitchPSes) == 1:
+                    return 'unison'
+                if len(pitchPSes) == 2:
+                    return interval.Interval(self.pitches[0], self.pitches[1]).niceName
+                else:
+                    return 'multiple octaves'
+            if len(pitchPSes) == 1:
+                return 'enharmonic unison'
+            else:
+                return 'enharmonic octaves'
+
+        ctn = chordTables.addressToCommonNames(cta)
         if cta.cardinality == 2:
+            pitchNames = {p.name for p in self.pitches}
+            pitchPSes = {p.ps for p in self.pitches}
+
+            # find two different pitchClasses
+            p0 = self.pitches[0]
+            p0pitchClass = p0.pitchClass
+            p1: pitch.Pitch
+            for p in self.pitches[1:]:
+                if p.pitchClass != p0pitchClass:
+                    p1 = p
+                    break
+            else:  # pragma: no cover
+                raise ChordException('Will never happen, just for typing.')
+
+            relevantInterval = interval.Interval(p0, p1)
+
+            if len(pitchNames) > 2:
+                # C4 E4 B#4, etc.
+                simpleUn = relevantInterval.chromatic.simpleUndirected
+                plural = 's' if simpleUn != 1 else ''
+                return f'{simpleUn} semitone{plural}'
+
+            if len(pitchPSes) > 2:
+                return relevantInterval.semiSimpleNiceName + ' with octave doublings'
+
             return interval.Interval(self.pitches[0], self.pitches[1]).niceName
 
-        ctn = chordTables.addressToCommonNames(cta)
         forteClass = self.forteClass
         # forteClassTn = self.forteClassTn
         enharmonicTests = {
@@ -4634,12 +4720,62 @@ class Chord(note.NotRest):
         >>> c5.pitchedCommonName
         'forte class 6-36B above C#'
 
+        >>> c4 = chord.Chord('D3 F3 A#3')
+        >>> c4.pitchedCommonName
+        'enharmonic equivalent to major triad above D'
+
+
+        A single pitch just returns that pitch name:
+
+        >>> chord.Chord(['D3']).pitchedCommonName
+        'D'
+
+        Unless there is more than one octave:
+
+        >>> chord.Chord('D3 D4').pitchedCommonName
+        'Perfect Octave above D'
+        >>> chord.Chord('D3 D4 D5').pitchedCommonName
+        'multiple octaves above D'
+
+
+        Two different pitches give interval names:
+
+        >>> chord.Chord('F3 C4').pitchedCommonName
+        'Perfect Fifth above F'
+
+        Compound intervals are used unless there are multiple octaves:
+
+        >>> chord.Chord('E-3 C5').pitchedCommonName
+        'Major Thirteenth above Eb'
+        >>> chord.Chord('E-3 C5 C6').pitchedCommonName
+        'Major Sixth with octave doublings above Eb'
+
+
+        These one-pitch-class and two-pitch-class chords with multiple enharmonics are unusual:
+
+        >>> chord.Chord('D#3 E-3').pitchedCommonName
+        'enharmonic unison above D#'
+        >>> chord.Chord('D#3 E-3 D#4').pitchedCommonName
+        'enharmonic octaves above D#'
+        >>> chord.Chord('D#3 E-3 E3').pitchedCommonName
+        '1 semitone above D#'
+        >>> chord.Chord('D#3 E-3 F3 G--4').pitchedCommonName
+        '2 semitones above D#'
+
+
         Changed in v5.5 -- octaves never included, flats are converted,
         special tools for enharmonics.
+        Changed in v6.5 -- special names for one and two-pitchClass chords.
         '''
         nameStr = self.commonName
+        if nameStr in ('note', 'unison'):
+            return self.pitches[0].name
+
 
-        if 'enharmonic' in nameStr or 'forte class' in nameStr:
+        if self.pitchClassCardinality <= 2 or (
+                'enharmonic' in nameStr
+                or 'forte class' in nameStr
+                or ' semitone' in nameStr):
             # root detection gives weird results for pitchedCommonName
             bass = self.bass()
             return '%s above %s' % (nameStr, bass.name.replace('-', 'b'))
diff --git a/music21/common/numberTools.py b/music21/common/numberTools.py
index 19904ed4e..7fcc28b6b 100644
--- a/music21/common/numberTools.py
+++ b/music21/common/numberTools.py
@@ -248,7 +248,6 @@ def opFrac(num):
     This is a performance critical operation. Do not alter it in any way without running
     many timing tests.
 
-
     >>> from fractions import Fraction
     >>> defaults.limitOffsetDenominator
     65535
@@ -298,7 +297,7 @@ def opFrac(num):
     elif num is None:
         return None
 
-    # class inheritance only check AFTER ifs...
+    # class inheritance only check AFTER ifs... this is redundant but highly optimized.
     elif isinstance(num, int):
         return num + 0.0
     elif isinstance(num, float):
diff --git a/music21/configure.py b/music21/configure.py
index 9d1164bc3..eb6bd1eb5 100644
--- a/music21/configure.py
+++ b/music21/configure.py
@@ -9,6 +9,7 @@
 # License:      BSD, see license.txt
 # ------------------------------------------------------------------------------
 import os
+import pathlib
 import re
 import time
 import sys
@@ -16,6 +17,8 @@ import unittest
 import textwrap
 import webbrowser
 
+from typing import List
+
 from importlib import reload  # Python 3.4
 
 import io
@@ -30,23 +33,21 @@ _MOD = 'configure'
 environLocal = environment.Environment(_MOD)
 
 _DOC_IGNORE_MODULE_OR_PACKAGE = True
+IGNORECASE = re.RegexFlag.IGNORECASE
 
 # ------------------------------------------------------------------------------
 # match finale name, which may be directory or something else
-reFinaleApp = re.compile(r'Finale.*.app',
-                         re.IGNORECASE)  # @UndefinedVariable
-reSibeliusApp = re.compile(r'Sibelius.app', re.IGNORECASE)  # @UndefinedVariable
-reFinaleExe = re.compile(r'Finale.*.exe',
-                         re.IGNORECASE)  # @UndefinedVariable
-reSibeliusExe = re.compile(r'Sibelius.exe', re.IGNORECASE)  # @UndefinedVariable
-reFinaleReaderApp = re.compile(r'Finale Reader.app', re.IGNORECASE)  # @UndefinedVariable
-reMuseScoreApp = re.compile(r'MuseScore.*.app', re.IGNORECASE)  # @UndefinedVariable
-reMuseScoreExe = re.compile(r'Musescore.*\\bin\\MuseScore.exe',
-                            re.IGNORECASE)  # @UndefinedVariable
+reFinaleApp = re.compile(r'Finale.*\.app', IGNORECASE)
+reSibeliusApp = re.compile(r'Sibelius\.app', IGNORECASE)
+reFinaleExe = re.compile(r'Finale.*\.exe', IGNORECASE)
+reSibeliusExe = re.compile(r'Sibelius\.exe', IGNORECASE)
+reFinaleReaderApp = re.compile(r'Finale Reader\.app', IGNORECASE)
+reMuseScoreApp = re.compile(r'MuseScore.*\.app', IGNORECASE)
+reMuseScoreExe = re.compile(r'Musescore.*\\bin\\MuseScore.*\.exe', IGNORECASE)
 
 urlMusic21 = 'http://web.mit.edu/music21'
 urlMuseScore = 'http://musescore.org'
-urlGettingStarted = 'http://web.mit.edu/music21/doc/'  # 'http://music21.readthedocs.org'
+urlGettingStarted = 'http://web.mit.edu/music21/doc/'
 urlMusic21List = 'http://groups.google.com/group/music21list'
 
 LINE_WIDTH = 78
@@ -407,17 +408,17 @@ class Dialog:
             self._promptHeader = msg
 
     def _askTryAgain(self, default=True, force=None):
-        '''What to do if input is incomplete
-
+        '''
+        What to do if input is incomplete
 
-        >>> d = configure.YesOrNo(default=True)
-        >>> d._askTryAgain(force='yes')
+        >>> prompt = configure.YesOrNo(default=True)
+        >>> prompt._askTryAgain(force='yes')
         True
-        >>> d._askTryAgain(force='n')
+        >>> prompt._askTryAgain(force='n')
         False
-        >>> d._askTryAgain(force='')  # gets default
+        >>> prompt._askTryAgain(force='')  # gets default
         True
-        >>> d._askTryAgain(force='blah')  # error gets false
+        >>> prompt._askTryAgain(force='blah')  # error gets false
         False
         '''
         # need to call a yes or no on using default
@@ -448,7 +449,14 @@ class Dialog:
                 div = ''
             else:
                 div = ':'
-            msg = '%s%s %s' % (header, div, msg)
+
+            if self._promptHeader.endswith('\n\n'):
+                div += '\n\n'
+            elif self._promptHeader.endswith('\n'):
+                div += '\n'
+            else:
+                div += ' '
+            msg = '%s%s%s' % (header, div, msg)
         return msg
 
     def _rawQueryPrepareFooter(self, msg=''):
@@ -786,7 +794,7 @@ class AskOpenInBrowser(YesOrNo):
             # override whatever is already in the prompt
             self._promptHeader = prompt
         else:  # else, append
-            msg = 'Open the following URL (%s) in a web browser?' % self._urlTarget
+            msg = 'Open the following URL (%s) in a web browser?\n' % self._urlTarget
             self.appendPromptHeader(msg)
 
     def _performAction(self, simulate=False):
@@ -968,14 +976,14 @@ class SelectFromList(Dialog):
         What to do if the selection list is empty. Only return True or False:
         if we should continue or not.
 
-        >>> d = configure.SelectFromList(default=True)
-        >>> d._askFillEmptyList(force='yes')
+        >>> prompt = configure.SelectFromList(default=True)
+        >>> prompt._askFillEmptyList(force='yes')
         True
-        >>> d._askFillEmptyList(force='n')
+        >>> prompt._askFillEmptyList(force='n')
         False
-        >>> d._askFillEmptyList(force='')  # no default, returns False
+        >>> prompt._askFillEmptyList(force='')  # no default, returns False
         False
-        >>> d._askFillEmptyList(force='blah')  # error gets false
+        >>> prompt._askFillEmptyList(force='blah')  # error gets false
         False
         '''
         # this does not do anything: customize in subclass
@@ -1161,6 +1169,7 @@ class AskAutoDownload(SelectFromList):
         '''
         result = self.getResult()
         if result in [1, 2, 3]:
+            # noinspection PyTypeChecker
             reload(environment)
             # us = environment.UserSettings()
             if result == 1:
@@ -1185,7 +1194,9 @@ class SelectFilePath(SelectFromList):
     def __init__(self, default=None, tryAgain=True, promptHeader=None):
         super().__init__(default=default, tryAgain=tryAgain, promptHeader=promptHeader)
 
-    def _getAppOSIndependent(self, comparisonFunction, path0, post):
+    def _getAppOSIndependent(self, comparisonFunction, path0: str, post: List[str],
+                             *,
+                             glob: str = '**/*'):
         '''
         Uses comparisonFunction to see if a file in path0 matches
         the RE embedded in comparisonFunction and if so manipulate the list
@@ -1193,62 +1204,48 @@ class SelectFilePath(SelectFromList):
 
         comparisonFunction = function (lambda function on path returning True/False)
         path0 = os-specific string
-        post = list of matching results.
-        '''
-        for sub1 in sorted(os.listdir(path0)):
-            path1 = os.path.join(path0, sub1)
-            if os.path.isdir(path1):
-                # on MacOS, .app files are actually directories; thus, look
-                # at these names directly
-                if comparisonFunction(sub1):
-                    post.append(path1)
-                    continue
-                # only go two levels deep in /Applications: all things there,
-                # and all things in directories stored there.
-                try:
-                    for sub2 in sorted(os.listdir(path1)):
-                        path2 = os.path.join(path1, sub2)
-                        if comparisonFunction(sub2):
-                            post.append(path2)
-                except OSError:
-                    print('Could not read paths inside %s' % path1)
-            else:
-                if comparisonFunction(sub1):
-                    post.append(path1)
+        post = list of matching results to fill
+        glob = string to glob for (default **/*, but **/*.exe on Windows and * on Mac).
+        '''
+        path0_as_path = pathlib.Path(path0)
+        for path1 in path0_as_path.glob(glob):
+            if comparisonFunction(str(path1)):
+                post.append(str(path1))
 
-    def _getDarwinApp(self, comparisonFunction):
+    def _getDarwinApp(self, comparisonFunction) -> List[str]:
         '''
         Provide a comparison function that returns True or False based on the file name.
         This looks at everything in Applications, as well as every directory in Applications
         '''
-        post = []
+        post: List[str] = []
         for path0 in ('/Applications', common.cleanpath('~/Applications')):
-            self._getAppOSIndependent(comparisonFunction, path0, post)
+            self._getAppOSIndependent(comparisonFunction, path0, post, glob='*')
         return post
 
-    def _getWinApp(self, comparisonFunction):
-        '''Provide a comparison function that returns True or False based on the file name.
+    def _getWinApp(self, comparisonFunction) -> List[str]:
+        '''
+        Provide a comparison function that returns True or False based on the file name.
         '''
         # provide a similar method to _getDarwinApp
-        post = []
+        post: List[str] = []
         environKeys = ('ProgramFiles', 'ProgramFiles(x86)', 'ProgramW6432')
         for possibleEnvironKey in environKeys:
             if possibleEnvironKey not in os.environ:
-                continue
+                continue  # Many do not define ProgramW6432
             environPath = os.environ[possibleEnvironKey]
             if environPath == '':
                 continue
-            self._getAppOSIndependent(comparisonFunction, environPath, post)
+            self._getAppOSIndependent(comparisonFunction, environPath, post, glob='**/*.exe')
 
         return post
 
     def _evaluateUserInput(self, raw):
-        '''Evaluate the user's string entry after parsing;
+        '''
+        Evaluate the user's string entry after parsing;
         do not return None: either return a valid response, default if available,
         IncompleteInput, NoInput objects.
 
         Here, we convert the user-selected number into a file path
-
         '''
         rawParsed = self._parseUserInput(raw)
         # if NoInput: and a default, return default
@@ -1283,7 +1280,7 @@ class SelectMusicXMLReader(SelectFilePath):
                                 promptHeader=promptHeader)
 
         # define platforms that this will run on
-        self._platforms = ['darwin']
+        self._platforms = ['darwin', 'win']
 
     def _rawIntroduction(self):
         '''
@@ -1292,7 +1289,7 @@ class SelectMusicXMLReader(SelectFilePath):
         return [
             'Defining an XML Reader permits automatically opening '
             + 'music21-generated MusicXML in an editor for display and manipulation when calling '
-            + 'the show() method. Setting this option is highly recommended.',
+            + 'the show() method. Setting this option is highly recommended. ',
             ' '
         ]
 
@@ -1301,16 +1298,16 @@ class SelectMusicXMLReader(SelectFilePath):
         Get all possible MusicXML Reader paths on Darwin (i.e., macOS)
         '''
         def comparisonFinale(x):
-            return reFinaleApp.match(x) is not None
+            return reFinaleApp.search(x) is not None
 
         def comparisonMuseScore(x):
-            return reMuseScoreApp.match(x) is not None
+            return reMuseScoreApp.search(x) is not None
 
         def comparisonFinaleReader(x):
-            return reFinaleReaderApp.match(x) is not None
+            return reFinaleReaderApp.search(x) is not None
 
         def comparisonSibelius(x):
-            return reSibeliusApp.match(x) is not None
+            return reSibeliusApp.search(x) is not None
 
         # order here results in ranks
         results = self._getDarwinApp(comparisonMuseScore)
@@ -1318,31 +1315,43 @@ class SelectMusicXMLReader(SelectFilePath):
         results += self._getDarwinApp(comparisonFinaleReader)
         results += self._getDarwinApp(comparisonSibelius)
 
-        return results
+        # de-duplicate
+        res = []
+        for one_path in results:
+            if one_path not in res:
+                res.append(one_path)
+
+        return res
 
     def _getMusicXMLReaderWin(self):
         '''
         Get all possible MusicXML Reader paths on Windows
         '''
         def comparisonFinale(x):
-            return reFinaleExe.match(x) is not None
+            return reFinaleExe.search(x) is not None
 
         def comparisonMuseScore(x):
-            return reMuseScoreExe.match(x) is not None
+            return reMuseScoreExe.search(x) is not None and 'crash-reporter' not in x
 
         def comparisonSibelius(x):
-            return reSibeliusExe.match(x) is not None
+            return reSibeliusExe.search(x) is not None
 
         # order here results in ranks
         results = self._getWinApp(comparisonMuseScore)
         results += self._getWinApp(comparisonFinale)
         results += self._getWinApp(comparisonSibelius)
 
-        return results
+        # de-duplicate (Windows especially can put the same environment var twice)
+        res = []
+        for one_path in results:
+            if one_path not in res:
+                res.append(one_path)
+
+        return res
 
     def _getMusicXMLReaderNix(self):
         '''
-        Get all possible Finale paths on Unix
+        Get all possible MusicXML Reader paths on Unix
         '''
         return []
 
@@ -1379,7 +1388,7 @@ class SelectMusicXMLReader(SelectFilePath):
             default=True,
             tryAgain=False,
             promptHeader='No available MusicXML readers are found on your system. '
-            + 'We recommend downloading and installing a reader before continuing.')
+            + 'We recommend downloading and installing a reader before continuing.\n\n')
         d.askUser(force=force)
         post = d.getResult()
         # can call regardless of result; will only function if result is True
@@ -1410,6 +1419,7 @@ class SelectMusicXMLReader(SelectFilePath):
         '''
         result = self.getResult()
         if result is not None and not isinstance(result, DialogError):
+            # noinspection PyTypeChecker
             reload(environment)
             # us = environment.UserSettings()
             # us['musicxmlPath'] = result  # automatically writes
@@ -1423,9 +1433,7 @@ class ConfigurationAssistant:
     '''
     Class for managing numerous configuration tasks.
     '''
-
     def __init__(self, simulate=False):
-
         self._simulate = simulate
         self._platform = common.getPlatform()
 
@@ -1592,54 +1600,54 @@ class TestExternal(unittest.TestCase):  # pragma: no cover
 
     def testYesOrNo(self):
         print()
-        environLocal.printDebug(['starting: YesOrNo()'])
+        print('starting: YesOrNo()')
         d = YesOrNo()
         d.askUser()
-        environLocal.printDebug(['getResult():', d.getResult()])
+        print('getResult():', d.getResult())
 
         print()
-        environLocal.printDebug(['starting: YesOrNo(default=True)'])
+        print('starting: YesOrNo(default=True)')
         d = YesOrNo(default=True)
         d.askUser()
-        environLocal.printDebug(['getResult():', d.getResult()])
+        print('getResult():', d.getResult())
 
         print()
-        environLocal.printDebug(['starting: YesOrNo(default=False)'])
+        print('starting: YesOrNo(default=False)')
         d = YesOrNo(default=False)
         d.askUser()
-        environLocal.printDebug(['getResult():', d.getResult()])
+        print('getResult():', d.getResult())
 
     def testSelectMusicXMLReader(self):
         print()
-        environLocal.printDebug(['starting: SelectMusicXMLReader()'])
+        print('starting: SelectMusicXMLReader()')
         d = SelectMusicXMLReader()
         d.askUser()
-        environLocal.printDebug(['getResult():', d.getResult()])
+        print('getResult():', d.getResult())
 
+    def testSelectMusicXMLReaderDefault(self):
         print()
-        environLocal.printDebug(['starting: SelectMusicXMLReader(default=1)'])
+        print('starting: SelectMusicXMLReader(default=1)')
         d = SelectMusicXMLReader(default=1)
         d.askUser()
-        environLocal.printDebug(['getResult():', d.getResult()])
+        print('getResult():', d.getResult())
 
     def testOpenInBrowser(self):
         print()
-        environLocal.printDebug(['starting: SelectMusicXMLReader()'])
         d = AskOpenInBrowser('http://mit.edu/music21')
         d.askUser()
-        environLocal.printDebug(['getResult():', d.getResult()])
+        print('getResult():', d.getResult())
         d.performAction()
 
     def testSelectMusicXMLReader2(self):
         print()
-        environLocal.printDebug(['starting: SelectMusicXMLReader()'])
+        print('starting: SelectMusicXMLReader()')
         d = SelectMusicXMLReader()
         d.askUser()
-        environLocal.printDebug(['getResult():', d.getResult()])
+        print('getResult():', d.getResult())
         d.performAction()
 
         print()
-        environLocal.printDebug(['starting: SelectMusicXMLReader()'])
+        print('starting: SelectMusicXMLReader() w/o results')
         d = SelectMusicXMLReader()
         # force request to user by returning no valid results
 
@@ -1648,10 +1656,11 @@ class TestExternal(unittest.TestCase):  # pragma: no cover
 
         d._getValidResults = getValidResults
         d.askUser()
-        environLocal.printDebug(['getResult():', d.getResult()])
+        print('getResult():', d.getResult())
         d.performAction()
 
     def testConfigurationAssistant(self):
+        print('Running ConfigurationAssistant all')
         configAsst = ConfigurationAssistant(simulate=True)
         configAsst.run()
 
@@ -1702,15 +1711,15 @@ class Test(unittest.TestCase):
         self.assertIsInstance(post, configure.BadConditions)
 
     def testRe(self):
-        g = reFinaleApp.match('Finale 2011.app')
+        g = reFinaleApp.search('Finale 2011.app')
         self.assertEqual(g.group(0), 'Finale 2011.app')
 
-        self.assertEqual(reFinaleApp.match('final blah 2011'), None)
+        self.assertEqual(reFinaleApp.search('final blah 2011'), None)
 
-        g = reFinaleApp.match('Finale.app')
+        g = reFinaleApp.search('Finale.app')
         self.assertEqual(g.group(0), 'Finale.app')
 
-        self.assertEqual(reFinaleApp.match('Final Cut 2017.app'), None)
+        self.assertEqual(reFinaleApp.search('Final Cut 2017.app'), None)
 
     def testConfigurationAssistant(self):
         unused_ca = ConfigurationAssistant(simulate=True)
@@ -1723,21 +1732,21 @@ class Test(unittest.TestCase):
 
     def testGetUserData(self):
         unused_d = AskSendInstallationReport()
-#         d.askUser()
-#         d.getResult()
-#         d.performAction()
+        # d.askUser()
+        # d.getResult()
+        # d.performAction()
 
     def testGetUserData2(self):
         unused_d = AskAutoDownload()
-#         d.askUser()
-#         d.getResult()
-#         d.performAction()
+        # d.askUser()
+        # d.getResult()
+        # d.performAction()
 
     def testAnyKey(self):
         unused_d = AnyKey()
-#         d.askUser()
-#         d.getResult()
-#         d.performAction()
+        # d.askUser()
+        # d.getResult()
+        # d.performAction()
 
 
 def run():
diff --git a/music21/duration.py b/music21/duration.py
index 26465e337..e26edbb55 100644
--- a/music21/duration.py
+++ b/music21/duration.py
@@ -2280,6 +2280,9 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
     def updateQuarterLength(self):
         '''
         Look to components and determine quarter length.
+
+        DEPRECATED -- this is no longer needed except by duration developers
+        and will be removed in v.7
         '''
         if self.linked is True:
             self._qtrLength = opFrac(self.quarterLengthNoTuplets * self.aggregateTupletMultiplier())
diff --git a/music21/environment.py b/music21/environment.py
index 67b512e0e..a69262e09 100644
--- a/music21/environment.py
+++ b/music21/environment.py
@@ -37,7 +37,6 @@ from xml.sax import saxutils
 from music21 import exceptions21
 from music21 import common
 
-
 _MOD = 'environment'
 
 
@@ -59,6 +58,7 @@ def etIndent(elem, level=0, spaces=2):
         if level and (not elem.tail or not elem.tail.strip()):
             elem.tail = i
 
+
 # -----------------------------------------------------------------------------
 
 
@@ -128,6 +128,7 @@ class LocalCorpusSettings(list):
             mdbpPart = ', cacheFilePath=' + repr(self.cacheFilePath)
         return f'LocalCorpusSettings({listRepr}{namePart}{mdbpPart})'
 
+
 # -----------------------------------------------------------------------------
 
 
@@ -403,7 +404,12 @@ class _EnvironmentCore:
             ]:
                 self.__setitem__(name, value)  # use for key checking
         elif platform == 'nix':
-            for name, value in [('lilypondPath', 'lilypond')]:
+            for name, value in [
+                ('lilypondPath', '/usr/bin/lilypond'),
+                ('musicxmlPath', '/usr/bin/mscore3'),
+                ('graphicsPath', '/usr/bin/xdg-open'),
+                ('pdfPath', '/usr/bin/xdg-open')
+            ]:
                 self.__setitem__(name, value)  # use for key checking
         elif platform == 'darwin':
             versionTuple = common.macOSVersion()
@@ -833,6 +839,7 @@ def envSingleton():
     '''
     return _environStorage['instance']
 
+
 # -----------------------------------------------------------------------------
 
 
@@ -1206,6 +1213,7 @@ class Environment:
         else:
             return 'unknown'
 
+
 # -----------------------------------------------------------------------------
 
 
@@ -1632,4 +1640,5 @@ _DOC_ORDER = [UserSettings, Environment, LocalCorpusSettings]
 
 if __name__ == '__main__':
     import music21
+
     music21.mainTest(Test)
diff --git a/music21/expressions.py b/music21/expressions.py
index 2ca2f1bfe..f327c503e 100644
--- a/music21/expressions.py
+++ b/music21/expressions.py
@@ -475,7 +475,7 @@ class GeneralMordent(Ornament):
             raise ExpressionException('Cannot realize a mordent if I do not know its direction')
         if self.size == '':
             raise ExpressionException('Cannot realize a mordent if there is no size given')
-        if srcObj.duration is None or srcObj.duration.quarterLength == 0:
+        if srcObj.duration.quarterLength == 0:
             raise ExpressionException('Cannot steal time from an object with no duration')
         if srcObj.duration.quarterLength < self.quarterLength * 2:
             raise ExpressionException('The note is not long enough to realize a mordent')
@@ -673,7 +673,7 @@ class Trill(Ornament):
         from music21 import key
         if self.size == '':
             raise ExpressionException('Cannot realize a trill if there is no size given')
-        if srcObj.duration is None or srcObj.duration.quarterLength == 0:
+        if srcObj.duration.quarterLength == 0:
             raise ExpressionException('Cannot steal time from an object with no duration')
         if srcObj.duration.quarterLength < 2 * self.quarterLength:
             raise ExpressionException('The note is not long enough to realize a trill')
@@ -829,7 +829,7 @@ class Turn(Ornament):
 
         if self.size is None:
             raise ExpressionException('Cannot realize a turn if there is no size given')
-        if srcObject.duration is None or srcObject.duration.quarterLength == 0:
+        if srcObject.duration.quarterLength == 0:
             raise ExpressionException('Cannot steal time from an object with no duration')
         if srcObject.duration.quarterLength < 4 * self.quarterLength:
             raise ExpressionException('The note is not long enough to realize a turn')
@@ -924,7 +924,7 @@ class GeneralAppoggiatura(Ornament):
         if self.size == '':
             raise ExpressionException(
                 'Cannot realize an Appoggiatura if there is no size given')
-        if srcObj.duration is None or srcObj.duration.quarterLength == 0:
+        if srcObj.duration.quarterLength == 0:
             raise ExpressionException('Cannot steal time from an object with no duration')
 
         newDuration = srcObj.duration.quarterLength / 2
diff --git a/music21/key.py b/music21/key.py
index 30ece2582..2081cf155 100644
--- a/music21/key.py
+++ b/music21/key.py
@@ -1072,9 +1072,12 @@ class Key(KeySignature, scale.DiatonicScale):
                        method='correlationCoefficient',
                        *args,
                        **keywords):
-        '''Provide a measure of tonal ambiguity for Key determined with one of many methods.
+        '''
+        Provide a measure of tonal ambiguity for Key
+        determined with one of many methods.
 
-        The `correlationCoefficient` assumes that the alternateInterpretations list has
+        The `correlationCoefficient` assumes that the
+        alternateInterpretations list has
         been filled from the use of a KeyWeightKeyAnalysis subclass.
 
         >>> littlePiece = converter.parse('tinyNotation: 4/4 c4 d e f g a b cc ee gg ee cc')
diff --git a/music21/midi/__init__.py b/music21/midi/__init__.py
index 009fce244..f7d007e7a 100644
--- a/music21/midi/__init__.py
+++ b/music21/midi/__init__.py
@@ -829,12 +829,20 @@ class MidiEvent:
                 self.parameter1 = byte1  # this is the controller id
                 self.parameter2 = byte2  # this is the controller value
             return midiBytes[3:]
-        else:
-            # NOTE_ON and NOTE_OFF
+        elif self.type == ChannelVoiceMessages.PITCH_BEND:
+            self.parameter1 = byte1  # least significant byte
+            self.parameter2 = byte2  # most significant byte
+            return midiBytes[3:]
+        elif self.type in (ChannelVoiceMessages.NOTE_ON, ChannelVoiceMessages.NOTE_OFF):
             # next two bytes:  pitch, velocity
             self.pitch = byte1
             self.velocity = byte2
             return midiBytes[3:]
+        elif self.type == ChannelVoiceMessages.POLYPHONIC_KEY_PRESSURE:
+            self.parameter1 = byte1  # pitch
+            self.parameter2 = byte2  # pressure
+            return midiBytes[3:]
+        raise TypeError(f'expected ChannelVoiceMessage, got {self.type}')  # pragma: no cover
 
     def read(self, midiBytes):
         r'''
@@ -1938,6 +1946,33 @@ class Test(unittest.TestCase):
         #    print(n, n.quarterLength)
         # s.show()
 
+    def testReadPolyphonicKeyPressure(self):
+        from music21 import midi
+
+        mt = midi.MidiTrack()
+        # Example string from MidiTrack doctest
+        mt.read(b'MTrk\x00\x00\x00\x16\x00\xff\x03\x00\x00'
+                + b'\xe0\x00@\x00\x90CZ\x88\x00\x80C\x00\x88\x00\xff/\x00')
+
+        pressure = MidiEvent()
+        pressure.type = ChannelVoiceMessages.POLYPHONIC_KEY_PRESSURE
+        pressure.channel = 1
+        pressure.parameter1 = 60
+        pressure.parameter2 = 90
+        mt.events.insert(len(mt.events) - 2, mt.events[-2])  # copy DeltaTime event
+        mt.events.insert(len(mt.events) - 2, pressure)
+
+        writingFile = midi.MidiFile()
+        writingFile.tracks = [mt]
+        byteStr = writingFile.writestr()
+        readingFile = midi.MidiFile()
+        readingFile.readstr(byteStr)
+
+        pressureEventRead = [e for e in readingFile.tracks[0].events
+                            if e.type == ChannelVoiceMessages.POLYPHONIC_KEY_PRESSURE][0]
+        self.assertEqual(pressureEventRead.parameter1, 60)
+        self.assertEqual(pressureEventRead.parameter2, 90)
+
 
 # ------------------------------------------------------------------------------
 # define presented order in documentation
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index 6196ee146..ceb8206b6 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -689,22 +689,39 @@ def midiEventsToInstrument(eventList):
     '''
     Convert a single MIDI event into a music21 Instrument object.
     '''
+    from music21 import midi as midiModule
+
     if not common.isListLike(eventList):
         event = eventList
     else:  # get the second event; first is delta time
         event = eventList[1]
 
     from music21 import instrument
+    decoded: str = ''
     try:
         if isinstance(event.data, bytes):
             # MuseScore writes MIDI files with null-terminated
             # instrument names.  Thus stop before the byte-0x0
             decoded = event.data.decode('utf-8').split('\x00')[0]
+            decoded = decoded.strip()
             i = instrument.fromString(decoded)
         else:
             i = instrument.instrumentFromMidiProgram(event.data)
     except (instrument.InstrumentException, UnicodeDecodeError):  # pragma: no cover
         i = instrument.Instrument()
+    # Set partName or instrumentName with literal value from parsing
+    if decoded:
+        # Except for lousy instrument names
+        if (
+            decoded.lower() in ('instrument', 'inst')
+            or decoded.lower().replace('instrument ', '').isdigit()
+            or decoded.lower().replace('inst ', '').isdigit()
+        ):
+            return i
+        elif event.type == midiModule.MetaEvents.SEQUENCE_TRACK_NAME:
+            i.partName = decoded
+        elif event.type == midiModule.MetaEvents.INSTRUMENT_NAME:
+            i.instrumentName = decoded
     return i
 
 
@@ -3216,6 +3233,13 @@ class Test(unittest.TestCase):
         out = midiFileToStream(mf)
         instruments = out.parts[0].getElementsByClass('Instrument')
         self.assertIsInstance(instruments[0], instrument.Oboe)
+        self.assertEqual(instruments[0].quarterLength, 0)
+
+        # Unrecognized instrument 'a'
+        dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive'
+        fp = dirLib / 'test15.mid'
+        s2 = converter.parse(fp)
+        self.assertEqual(s2.parts[0].partName, 'a')
 
     def testImportZeroDurationNote(self):
         '''
@@ -3260,6 +3284,18 @@ class Test(unittest.TestCase):
         i = midiEventsToInstrument(event)
         self.assertIsInstance(i, instrument.Flute)
 
+    def testLousyInstrumentName(self):
+        from music21 import midi as midiModule
+
+        lousyNames = ('    ', 'Instrument 20', 'Instrument', 'Inst 2', 'instrument')
+        for name in lousyNames:
+            with self.subTest(name=name):
+                event = midiModule.MidiEvent()
+                event.data = bytes(name, 'utf-8')
+                event.type = midiModule.MetaEvents.INSTRUMENT_NAME
+                i = midiEventsToInstrument(event)
+                self.assertIsNone(i.instrumentName)
+
 
 # ------------------------------------------------------------------------------
 _DOC_ORDER = [streamToMidiFile, midiFileToStream]
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 467b8f70c..7e98cb4db 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -3950,6 +3950,8 @@ class MeasureParser(XMLParserBase):
                 showNumber = mxTuplet.get('show-number')
                 if showNumber is not None and showNumber == 'none':
                     t.tupletActualShow = None
+                    if bracketMaybe is None:
+                        t.bracket = False
                 elif showNumber is not None and showNumber == 'both':
                     t.tupletNormalShow = 'number'
 
@@ -6374,6 +6376,16 @@ class Test(unittest.TestCase):
         self.assertEqual(repr(nList[12].duration.tuplets),
                          '(<music21.duration.Tuplet 3/2/eighth>,)')
 
+    def testImpliedTuplet(self):
+        from music21 import converter
+        from music21.musicxml import testPrimitive
+
+        s = converter.parse(testPrimitive.tupletsImplied)
+        # First tuplet group of 3 is silent on bracket and show-number: draw bracket
+        # Second tuplet group of 3 is silent on bracket but show-number="none": don't draw bracket
+        tuplets = [n.duration.tuplets[0] for n in s.flat.notes]
+        self.assertEqual([tup.bracket for tup in tuplets], [True, True, True, False, False, False])
+
     def test34MeasureRestWithoutTag(self):
         from xml.etree.ElementTree import fromstring as EL
 
diff --git a/music21/search/base.py b/music21/search/base.py
index 757a1647b..42084e00f 100644
--- a/music21/search/base.py
+++ b/music21/search/base.py
@@ -449,6 +449,7 @@ def noteNameSearch(thisStreamOrIterator, searchList):
 
 
 def noteNameRhythmicSearch(thisStreamOrIterator, searchList):
+    # noinspection PyShadowingNames
     '''
     >>> thisStream = converter.parse('tinynotation: 3/4 c4 d8 e c d e f c D E c c4 d# e')
     >>> searchList = [note.Note('C'), note.Note('D'), note.Note('E')]
@@ -485,6 +486,7 @@ def noteNameRhythmicSearch(thisStreamOrIterator, searchList):
 
 
 def approximateNoteSearch(thisStream, otherStreams):
+    # noinspection PyShadowingNames
     '''
     searches the list of otherStreams and returns an ordered list of matches
     (each stream will have a new property of matchProbability to show how
@@ -555,6 +557,7 @@ def approximateNoteSearchNoRhythm(thisStream, otherStreams):
 
 
 def approximateNoteSearchOnlyRhythm(thisStream, otherStreams):
+    # noinspection PyShadowingNames
     '''
     searches the list of otherStreams and returns an ordered list of matches
     (each stream will have a new property of matchProbability to show how
@@ -590,6 +593,7 @@ def approximateNoteSearchOnlyRhythm(thisStream, otherStreams):
 
 
 def approximateNoteSearchWeighted(thisStream, otherStreams):
+    # noinspection PyShadowingNames
     '''
     searches the list of otherStreams and returns an ordered list of matches
     (each stream will have a new property of matchProbability to show how
@@ -666,7 +670,7 @@ def translateStreamToString(inputStreamOrIterator, returnMeasures=False):
 
 
 def translateDiatonicStreamToString(inputStreamOrIterator, returnMeasures=False):
-    # noinspection SpellCheckingInspection
+    # noinspection SpellCheckingInspection, PyShadowingNames
     r'''
     Translates a Stream or StreamIterator of Notes and Rests only into a string,
     encoding only the .step (no accidental or octave) and whether
@@ -746,6 +750,7 @@ def translateDiatonicStreamToString(inputStreamOrIterator, returnMeasures=False)
 
 
 def translateIntervalsAndSpeed(inputStream, returnMeasures=False):
+    # noinspection PyShadowingNames
     r'''
     Translates a Stream (not StreamIterator) of Notes and Rests only into a string,
     encoding only the chromatic distance from the last note and whether
@@ -841,10 +846,15 @@ def translateStreamToStringNoRhythm(inputStream, returnMeasures=False):
     takes a stream or streamIterator of notesAndRests only and returns
     a string for searching on, using translateNoteToByte.
 
-    >>> s = converter.parse("tinynotation: 4/4 c4 d e FF a' b-")
+    >>> s = converter.parse("tinynotation: 4/4 c4 d e FF a'2 b-2")
     >>> sn = s.flat.notesAndRests
     >>> search.translateStreamToStringNoRhythm(sn)
     '<>@)QF'
+
+    With returnMeasures, will return a tuple of bytes and a list of measure numbers:
+
+    >>> search.translateStreamToStringNoRhythm(sn, returnMeasures=True)
+    ('<>@)QF', [1, 1, 1, 1, 2, 2])
     '''
     b = ''
     measures = []
@@ -863,7 +873,6 @@ def translateStreamToStringOnlyRhythm(inputStream, returnMeasures=False):
     takes a stream or streamIterator of notesAndRests only and returns
     a string for searching on.
 
-
     >>> s = converter.parse("tinynotation: 3/4 c4 d8 e16 FF8. a'8 b-2.")
     >>> sn = s.flat.notesAndRests
     >>> streamString = search.translateStreamToStringOnlyRhythm(sn)
@@ -914,6 +923,7 @@ def translateNoteToByte(n):
 
 
 def translateNoteWithDurationToBytes(n, includeTieByte=True):
+    # noinspection PyShadowingNames
     '''
     takes a note.Note object and translates it to a three-byte representation.
 
@@ -922,7 +932,6 @@ def translateNoteWithDurationToBytes(n, includeTieByte=True):
     :func:`~music21.search.base.translateDurationToBytes`)
     followed by 's', 'c', or 'e' if includeTieByte is True and there is a tie.
 
-
     >>> n = note.Note('C4')
     >>> n.duration.quarterLength = 3  # dotted half
     >>> trans = search.translateNoteWithDurationToBytes(n)
@@ -939,8 +948,6 @@ def translateNoteWithDurationToBytes(n, includeTieByte=True):
     >>> trans = search.translateNoteWithDurationToBytes(n, includeTieByte=False)
     >>> trans
     '<_'
-
-
     '''
     firstByte = translateNoteToByte(n)
     secondByte = translateDurationToBytes(n)
diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py
index a3fd3233b..1d0bb2e6d 100644
--- a/music21/stream/__init__.py
+++ b/music21/stream/__init__.py
@@ -28,7 +28,7 @@ import warnings
 import sys
 
 from fractions import Fraction
-from typing import Union, List, Optional, Set
+from typing import Union, List, Optional, Set, Tuple, Sequence
 
 from music21 import base
 
@@ -215,8 +215,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         self._unlinkedDuration = None
 
         self.autoSort = True
-        # should isFlat become readonly?
-        self.isFlat = True  # does it have no embedded Streams
 
         # these should become part of style or something else...
         self.definesExplicitSystemBreaks = False
@@ -243,12 +241,12 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 return str(self.id)
             else:
                 return hex(self.id)
-        else:
+        else:  # pragma: no cover
             return ''
 
     def write(self, fmt=None, fp=None, **keywords):
         # ...    --- see base.py calls .write(
-        if self.isSorted is False and self.autoSort:
+        if self.isSorted is False and self.autoSort:  # pragma: no cover
             self.sort()
         return super().write(fmt=fmt, fp=fp, **keywords)
 
@@ -374,6 +372,18 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         >>> a['green'] is b
         True
+
+        If not found, a KeyError will be raised:
+
+        >>> a['purple']
+        Traceback (most recent call last):
+        KeyError: 'provided key (purple) does not match any id or group'
+
+        >>> a[layout.StaffLayout]
+        Traceback (most recent call last):
+        KeyError: "provided class (<class 'music21.layout.StaffLayout'>) does
+            not match any contained Objects"
+
         '''
         # need to sort if not sorted, as this call may rely on index positions
         if not self.isSorted and self.autoSort:
@@ -391,8 +401,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                     match = self.elements[k]
                 except IndexError:
                     raise StreamException(
-                        'attempting to access index %s while elements is of size %s' %
-                        (k, len(self.elements)))
+                        f'attempting to access index {k} '
+                        + f'while elements is of size {len(self.elements)}'
+                    )
             # setting active site as cautionary measure
             self.coreSelfActiveSite(match)
             return match
@@ -402,8 +413,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             # locations
             try:
                 found = self.cloneEmpty(derivationMethod='__getitem__')
-            except TypeError:
-                raise StreamException(('Error in defining class: %r. ' % self.__class__)
+            except TypeError:  # pragma: no cover
+                raise StreamException(f'Error in defining class: {self.__class__}'
                                       + 'Stream subclasses and Music21Objects cannot have required '
                                       + 'arguments in __init__')
             for e in self.elements[k]:
@@ -422,14 +433,14 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 if groupStream:
                     return groupStream.stream()
                 else:
-                    raise KeyError('provided key (%s) does not match any id or group' % k)
+                    raise KeyError(f'provided key ({k}) does not match any id or group')
         elif isinstance(k, type(type)):
             # assume it is a class name
             classStream = self.iter.getElementsByClass(k)
             if classStream:
                 return classStream.stream()
             else:
-                raise KeyError('provided class (%s) does not match any contained Objects' % k)
+                raise KeyError(f'provided class ({k}) does not match any contained Objects')
 
     def __contains__(self, el):
         '''
@@ -464,14 +475,51 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 return True
         return False
 
-    def _getElements(self):
+    @property
+    def elements(self) -> Tuple[base.Music21Object]:
         '''
-        Combines the two storage lists, _elements and _endElements, such that
-        they appear as a single list.
+        .elements is a Tuple representing the elements contained in the Stream.
+
+        Directly getting, setting, and manipulating this Tuple is
+        reserved for advanced usage. Instead, use the the
+        provided high-level methods.  The elements retrieved here may not
+        have this stream as an activeSite, therefore they might not be properly ordered.
+
+        In other words:  Don't use unless you really know what you're doing.
+        Treat a Stream like a list!
+
+        When setting .elements, a list of Music21Objects can be provided, or a complete Stream.
+        If a complete Stream is provided, elements are extracted
+        from that Stream. This has the advantage of transferring
+        offset correctly and getting elements stored at the end.
+
+        >>> a = stream.Stream()
+        >>> a.repeatInsert(note.Note('C'), list(range(10)))
+        >>> b = stream.Stream()
+        >>> b.repeatInsert(note.Note('D'), list(range(10)))
+        >>> b.offset = 6
+        >>> c = stream.Stream()
+        >>> c.repeatInsert(note.Note('E'), list(range(10)))
+        >>> c.offset = 12
+        >>> b.insert(c)
+        >>> b.isFlat
+        False
+
+        >>> a.isFlat
+        True
+
+        Assigning from a Stream works well, and is actually much safer than assigning
+        from `.elements` of the other Stream, since the active sites may have changed
+        of that stream's elements in the meantime.
+
+        >>> a.elements = b
+        >>> a.isFlat
+        False
 
-        Note that by the time you call .elements
-        the offsets
+        >>> len(a.flat.notes) == len(b.flat.notes) == 20
+        True
         '''
+        # combines _elements and _endElements into one.
         if 'elements' not in self._cache or self._cache['elements'] is None:
             # this list concatenation may take time; thus, only do when
             # coreElementsChanged has been called
@@ -480,7 +528,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             self._cache['elements'] = self._elements + self._endElements
         return tuple(self._cache['elements'])
 
-    def _setElements(self, value):
+    @elements.setter
+    def elements(self, value: Union['Stream', Sequence[base.Music21Object]]):
         '''
         Sets this streams elements to the elements in another stream (just give
         the stream, not the stream's .elements), or to a list of elements.
@@ -528,49 +577,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 self.coreSelfActiveSite(e)
         self.coreElementsChanged()
 
-    elements = property(_getElements, _setElements,
-                        doc='''
-        .elements is a list representing the elements contained in the Stream.
-
-        Directly getting, setting, and manipulating this list is
-        reserved for advanced usage. Instead, use the the
-        provided high-level methods.
-
-        In other words:  Don't use unless you really know what you're doing.
-        Treat a Stream like a list!
-
-        When setting .elements, a
-        list of Music21Objects can be provided, or a complete Stream.
-        If a complete Stream is provided, elements are extracted
-        from that Stream. This has the advantage of transferring
-        offset correctly and getting _endElements.
-
-        >>> a = stream.Stream()
-        >>> a.repeatInsert(note.Note('C'), list(range(10)))
-        >>> b = stream.Stream()
-        >>> b.repeatInsert(note.Note('D'), list(range(10)))
-        >>> b.offset = 6
-        >>> c = stream.Stream()
-        >>> c.repeatInsert(note.Note('E'), list(range(10)))
-        >>> c.offset = 12
-        >>> b.insert(c)
-        >>> b.isFlat
-        False
-
-        >>> a.isFlat
-        True
-
-        Assigning from a Stream works well.
-
-        >>> a.elements = b
-        >>> a.isFlat
-        False
-
-        >>> len(a.flat.notes) == len(b.flat.notes) == 20
-        True
-
-        :rtype: list(base.Music21Object)
-        ''')
 
     def __setitem__(self, k, value):
         '''
@@ -616,10 +622,10 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             self.coreElementsChanged()
 
     def __delitem__(self, k):
-        '''Delete element at an index position. Index positions are based
+        '''
+        Delete element at an index position. Index positions are based
         on positions in self.elements.
 
-
         >>> a = stream.Stream()
         >>> a.repeatInsert(note.Note('C'), list(range(10)))
         >>> del a[0]
@@ -637,34 +643,45 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         to actually be at the end of the Stream. This may be a problem that
         makes this method not so useful?
 
-
         >>> a = stream.Part()
-        >>> a.repeatInsert(note.Note('C'), list(range(5)))
+        >>> a.repeatInsert(note.Note('C'), [0, 1])
         >>> b = stream.Stream()
-        >>> b.repeatInsert(note.Note('G'), list(range(5)))
+        >>> b.repeatInsert(note.Note('G'), [0, 1, 2])
         >>> c = a + b
-        >>> c.pitches[0:4]  # autoSort is True, thus a sorted version results
+        >>> c.pitches  # autoSort is True, thus a sorted version results
         [<music21.pitch.Pitch C>,
          <music21.pitch.Pitch G>,
          <music21.pitch.Pitch C>,
+         <music21.pitch.Pitch G>,
          <music21.pitch.Pitch G>]
         >>> len(c.notes)
-        10
-
+        5
 
         The autoSort of the first stream becomes the autoSort of the
         destination.  The class of the first becomes the class of the destination.
 
-
         >>> a.autoSort = False
         >>> d = a + b
         >>> [str(p) for p in d.pitches]
-        ['C', 'C', 'C', 'C', 'C', 'G', 'G', 'G', 'G', 'G']
+        ['C', 'C', 'G', 'G', 'G']
         >>> d.__class__.__name__
         'Part'
 
+        Works with Streams with Store at end, which does put both at the end.
+
+        >>> a.autoSort = True
+        >>> a.storeAtEnd(bar.Barline('final'))
+        >>> b.storeAtEnd(clef.TrebleClef())
+        >>> f = a + b
+        >>> f.show('text')
+        {0.0} <music21.note.Note C>
+        {0.0} <music21.note.Note G>
+        {1.0} <music21.note.Note C>
+        {1.0} <music21.note.Note G>
+        {2.0} <music21.note.Note G>
+        {3.0} <music21.bar.Barline type=final>
+        {3.0} <music21.clef.TrebleClef>
         '''
-        # TODO: check class of other first
         if other is None or not isinstance(other, Stream):
             raise TypeError('cannot concatenate a Stream with a non-Stream')
 
@@ -718,31 +735,11 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         return False
 
     # ------------------------------
-    def _getClef(self):
-        clefList = self.iter.getElementsByClass('Clef').getElementsByOffset(0)
-        # casting to list added 20microseconds...
-        if not clefList:
-            return None
-        else:
-            return clefList[0]
-
-    def _setClef(self, clefObj):
-        # if clef is None; remove object?
-        oldClef = self._getClef()
-        if oldClef is not None:
-            # environLocal.printDebug(['removing clef', oldClef])
-            junk = self.pop(self.index(oldClef))
-        if clefObj is None:
-            # all that is needed is to remove the old clef
-            # there is no new clef - suppresses the clef of a stream
-            return
-        self.insert(0.0, clefObj)
-        # for some reason needed to make sure that sorting of Clef happens before TimeSignature
-        # TODO: Test if this can be deleted...
-        self.coreElementsChanged()
-
-    clef = property(_getClef, _setClef, doc='''
-        Finds or sets a :class:`~music21.clef.Clef` at offset 0.0 in the measure:
+    @property
+    def clef(self) -> Optional['music21.clef.Clef']:
+        '''
+        Finds or sets a :class:`~music21.clef.Clef` at offset 0.0 in the Stream
+        (generally a Measure):
 
         >>> m = stream.Measure()
         >>> m.number = 10
@@ -760,7 +757,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> m.clef.sign
         'F'
 
-
         And the TrebleClef is no longer in the measure:
 
         >>> thisTrebleClef.getOffsetBySite(m)
@@ -768,7 +764,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         music21.sites.SitesException: an entry for this object <music21.clef.TrebleClef> is not
               stored in stream <music21.stream.Measure 10 offset=0.0>
 
-
         The `.clef` appears in a `.show()` or other call
         just like any other element
 
@@ -776,14 +771,74 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> m.show('text')
         {0.0} <music21.clef.BassClef>
         {0.0} <music21.note.Note D#>
-        ''')
+        '''
+        clefList = self.iter.getElementsByClass('Clef').getElementsByOffset(0)
+        # casting to list added 20microseconds...
+        if not clefList:
+            return None
+        else:
+            return clefList[0]
 
-    def _getTimeSignature(self):
+    @clef.setter
+    def clef(self, clefObj: Optional['music21.clef.Clef']):
+        # if clef is None; remove object?
+        oldClef = self.clef
+        if oldClef is not None:
+            # environLocal.printDebug(['removing clef', oldClef])
+            junk = self.pop(self.index(oldClef))
+        if clefObj is None:
+            # all that is needed is to remove the old clef
+            # there is no new clef - suppresses the clef of a stream
+            return
+        self.insert(0.0, clefObj)
+        # for some reason needed to make sure that sorting of Clef happens before TimeSignature
+        # TODO: Test if this can be deleted...
+        self.coreElementsChanged()
+
+    @property
+    def timeSignature(self):
         '''
-        >>> a = stream.Measure()
-        >>> a.timeSignature = meter.TimeSignature('2/4')
-        >>> a.timeSignature.numerator, a.timeSignature.denominator
+        Gets or sets the timeSignature at offset 0.0 of the Stream (generally a Measure)
+
+        >>> m1 = stream.Measure(number=1)
+        >>> m1.timeSignature = meter.TimeSignature('2/4')
+        >>> m1.timeSignature.numerator, m1.timeSignature.denominator
         (2, 4)
+        >>> m1.show('text')
+        {0.0} <music21.meter.TimeSignature 2/4>
+
+        Setting timeSignature to None removes any TimeSignature at offset 0.0:
+
+        >>> m1.timeSignature = None
+        >>> m1.elements
+        ()
+
+
+        Only the time signature at offset 0 is found:
+
+        >>> m2 = stream.Measure(number=2)
+        >>> m2.insert(0.0, meter.TimeSignature('5/4'))
+        >>> m2.insert(2.0, meter.TimeSignature('7/4'))
+        >>> ts = m2.timeSignature
+        >>> ts.numerator, ts.denominator
+        (5, 4)
+
+        >>> m2.timeSignature = meter.TimeSignature('2/8')
+        >>> m2.timeSignature
+        <music21.meter.TimeSignature 2/8>
+
+        After setting a new `.timeSignature`, the old one is no longer in the Stream:
+
+        >>> ts in m2
+        False
+
+        This property is not recursive, so a Part will not have the time signature of
+        the measure within it:
+
+        >>> p = stream.Part()
+        >>> p.append(m2)
+        >>> p.timeSignature is None
+        True
         '''
         # there could be more than one
         tsList = self.iter.getElementsByClass('TimeSignature').getElementsByOffset(0)
@@ -795,18 +850,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         else:
             return tsList[0]
 
-    def _setTimeSignature(self, tsObj):
-        '''
-        >>> a = stream.Measure()
-        >>> a.timeSignature = meter.TimeSignature('5/4')
-        >>> a.timeSignature.numerator, a.timeSignature.denominator
-        (5, 4)
-        >>> a.timeSignature = meter.TimeSignature('2/8')
-        >>> a.timeSignature.numerator, a.timeSignature.denominator
-        (2, 8)
-
-        '''
-        oldTimeSignature = self._getTimeSignature()
+    @timeSignature.setter
+    def timeSignature(self, tsObj: Optional['music21.meter.TimeSignature']):
+        oldTimeSignature = self.timeSignature
         if oldTimeSignature is not None:
             # environLocal.printDebug(['removing ts', oldTimeSignature])
             junk = self.pop(self.index(oldTimeSignature))
@@ -816,42 +862,52 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             return
         self.insert(0, tsObj)
 
-    timeSignature = property(_getTimeSignature, _setTimeSignature)
-
-    def _getKeySignature(self):
+    @property
+    def keySignature(self) -> Optional['music21.key.KeySignature']:
         '''
-        Find or set a Key or KeySignature at offset 0 of a stream.
+        Find or set a Key or KeySignature at offset 0.0 of a stream.
 
         >>> a = stream.Measure()
-        >>> a.keySignature = key.KeySignature(2)
-        >>> a.keySignature.sharps
-        2
+        >>> a.keySignature = key.KeySignature(-2)
+        >>> ks = a.keySignature
+        >>> ks.sharps
+        -2
+        >>> a.show('text')
+        {0.0} <music21.key.KeySignature of 2 flats>
 
         A key.Key object can be used instead of key.KeySignature,
         since the former derives from the latter.
 
-        >>> a.keySignature = key.Key('E-', 'major')
-        >>> a.keySignature.sharps
-        -3
+        >>> a.keySignature = key.Key('E', 'major')
+        >>> for k in a:
+        ...     print(k.offset, repr(k))
+        0.0 <music21.key.Key of E major>
 
-        Setting a new key signature replaces any previous ones:
+        Notice that setting a new key signature replaces any previous ones:
 
         >>> len(a.getElementsByClass('KeySignature'))
         1
+
+        `.keySignature` can be set to None:
+
+        >>> a.keySignature = None
+        >>> a.keySignature is None
+        True
         '''
         try:
             return next(self.iter.getElementsByClass('KeySignature').getElementsByOffset(0))
         except StopIteration:
             return None
 
-    def _setKeySignature(self, keyObj):
+    @keySignature.setter
+    def keySignature(self, keyObj: Optional['music21.key.KeySignature']):
         '''
         >>> a = stream.Measure()
         >>> a.keySignature = key.KeySignature(6)
         >>> a.keySignature.sharps
         6
         '''
-        oldKey = self._getKeySignature()
+        oldKey = self.keySignature
         if oldKey is not None:
             # environLocal.printDebug(['removing key', oldKey])
             junk = self.pop(self.index(oldKey))
@@ -861,8 +917,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             return
         self.insert(0, keyObj)
 
-    keySignature = property(_getKeySignature, _setKeySignature)
-
     def clear(self):
         '''
         Remove all elements in a stream.
@@ -962,8 +1016,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         Only a single class name can be given.
 
-        Possibly to be deprecated in v.6
-
         >>> s = stream.Stream()
         >>> s.append(meter.TimeSignature('5/8'))
         >>> s.append(note.Note('d-2'))
@@ -972,12 +1024,18 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         True
         >>> s.hasElementOfClass('Measure')
         False
+
+        To be deprecated in v.7 -- to be removed in version 8, use:
+
+        >>> bool(s.getElementsByClass('TimeSignature'))
+        True
+        >>> bool(s.getElementsByClass('Measure'))
+        False
+
+        forceFlat does nothing, while getElementsByClass can be done on recurse()
         '''
         # environLocal.printDebug(['calling hasElementOfClass()', className])
-        for e in self._elements:
-            if className in e.classSet:
-                return True
-        for e in self._endElements:
+        for e in self.elements:
             if className in e.classSet:
                 return True
         return False
@@ -1083,20 +1141,17 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         else:
             self._cache['index'] = {}
 
-        # TODO: possibly replace by binary search
-        if common.isNum(el):
-            objId = el
-        else:
-            objId = id(el)
+        objId = id(el)
 
         count = 0
+
         for e in self._elements:
-            if id(e) == objId:
+            if e is el:
                 self._cache['index'][objId] = count
                 return count
             count += 1
         for e in self._endElements:
-            if id(e) == objId:
+            if e is el:
                 self._cache['index'][objId] = count
                 return count  # this is the index
             count += 1  # cumulative indices
@@ -1219,11 +1274,23 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         {4.0} <music21.stream.Measure 2 offset=4.0>
         <BLANKLINE>
 
+
+        Can also remove elements stored at end:
+
+        >>> streamWithBarline = stream.Stream(note.Note())
+        >>> barline = bar.Barline('final')
+        >>> streamWithBarline.storeAtEnd(barline)
+        >>> barline in streamWithBarline
+        True
+        >>> streamWithBarline.remove(barline)
+        >>> barline in streamWithBarline
+        False
+
         Changed in v5.3 -- firstMatchOnly removed -- impossible to have element
         in stream twice.  recurse and shiftOffsets changed to keywordOnly arguments
         '''
         # experimental
-        if self._mutable is False:
+        if self._mutable is False:  # pragma: no cover
             raise ImmutableStreamException('Cannot remove from an immutable stream')
         # TODO: Next to clean up... a doozy -- filter out all the different options.
 
@@ -1252,8 +1319,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             except StreamException:
                 if recurse is True:
                     for s in self.recurse(streamsOnly=True):
-                        if s is self:
-                            continue
                         try:
                             indexInStream = s.index(target)
                             s.remove(target)
@@ -1282,7 +1347,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
                 try:
                     del self._offsetDict[id(match)]
-                except KeyError:
+                except KeyError:  # pragma: no cover
                     pass
                 self.coreElementsChanged(clearIsSorted=False)
                 match.sites.remove(self)
@@ -1311,20 +1376,17 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             #     pass  # This should maybe just call a function renumberMeasures
         self.coreElementsChanged(clearIsSorted=False)
 
-    def pop(self, index):
+    def pop(self, index: int) -> 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)
         >>> len(a)
         9
-
-        :rtype: base.Music21Object
         '''
         eLen = len(self._elements)
         # if less then base length, its in _elements
@@ -1337,7 +1399,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         try:
             del self._offsetDict[id(post)]
-        except KeyError:
+        except KeyError:  # pragma: no cover
             pass
 
         post.sites.remove(self)
@@ -1364,7 +1426,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 removeElement = sectionList.pop(popIndex)
                 try:
                     del self._offsetDict[id(removeElement)]
-                except KeyError:
+                except KeyError:  # pragma: no cover
                     pass
 
                 # TODO: to make recursive, store a tuple of active sites and index
@@ -1374,7 +1436,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         # call elements changed once; sorted arrangement has not changed
         self.coreElementsChanged(clearIsSorted=False)
 
-    def removeByClass(self, classFilterList):
+    def removeByClass(self, classFilterList) -> None:
         '''
         Remove all elements from the Stream
         based on one or more classes given
@@ -1404,7 +1466,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         5
         '''
         elFilter = self.iter.getElementsByClass(classFilterList)
-        return self._removeIteration(elFilter)
+        self._removeIteration(elFilter)
 
     def removeByNotOfClass(self, classFilterList):
         '''
@@ -1524,7 +1586,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 continue
 
             origin = e.derivation.origin
-            if origin is None:
+            if origin is None:  # pragma: no cover
                 continue  # should not happen...
 
             if origin.sites.hasSpannerSite():
@@ -1538,7 +1600,14 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 # this must be done here, not when originally copying
                 e.purgeOrphans(excludeStorageStreams=False)
 
-    def setElementOffset(self, element, offset, *, addElement=False, setActiveSite=True):
+    def setElementOffset(
+        self,
+        element: base.Music21Object,
+        offset: Union[int, float, Fraction, str],
+        *,
+        addElement=False,
+        setActiveSite=True
+    ):
         '''
         Sets the Offset for an element, very quickly.
 
@@ -1573,10 +1642,13 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         Changed in v5.5 -- also sets .activeSite for the element unless setActiveSite is False
         '''
+        # Note: not documenting 'highestTime' is on purpose, since can only be done for
+        # elements already stored at end.  Infinite loop.
         try:
             offset = opFrac(offset)
         except TypeError:
-            pass
+            if offset not in core.OFFSET_STRING_VALUES:  # pragma: no cover
+                raise StreamException(f'Cannot set offset to {offset!r} for {element}')
 
         idEl = id(element)
         if not addElement and idEl not in self._offsetDict:
@@ -1650,18 +1722,23 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                     'an entry for this object 0x%x is not stored in stream %s' %
                     (id(element), self))
 
-        if stringReturns is False and o in ('highestTime', 'lowestOffset', 'highestOffset'):
+        if stringReturns is False and o in core.OFFSET_STRING_VALUES:
             try:
                 return getattr(self, o)
-            except AttributeError:
+            except AttributeError:  # pragma: no cover
                 raise base.SitesException(
                     'attempted to retrieve a bound offset with a string '
                     + 'attribute that is not supported: %s' % o)
         else:
             return o
 
-    def insert(self, offsetOrItemOrList, itemOrNone=None,
-                     ignoreSort=False, setActiveSite=True):
+    def insert(self,
+               offsetOrItemOrList,
+               itemOrNone=None,
+               *,
+               ignoreSort=False,
+               setActiveSite=True
+               ):
         '''
         Inserts an item(s) at the given offset(s).
 
@@ -1707,14 +1784,25 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> len(st3)
         2
 
-        OMIT_FROM_DOCS
+        Raises an error if offset is not a number
+
+        >>> stream.Stream().insert('l', note.Note('B'))
+        Traceback (most recent call last):
+        music21.exceptions21.StreamException: Offset 'l' must be a number.
 
-        Raise an error if offset is not a number
+        ...or if the object is not a music21 object (or a list of them)
 
-        >>> stream.Stream().insert('l', 'g')
+        >>> stream.Stream().insert(3.3, 'hello')
         Traceback (most recent call last):
-        music21.exceptions21.StreamException: ...
+        music21.exceptions21.StreamException: to put a non Music21Object in a stream,
+            create a music21.ElementWrapper for the item
 
+        The error message is slightly different in the one-element form:
+
+        >>> stream.Stream().insert('hello')
+        Traceback (most recent call last):
+        music21.exceptions21.StreamException: Cannot insert item 'hello' to
+            stream -- is it a music21 object?
         '''
         # environLocal.printDebug(['self', self, 'offsetOrItemOrList',
         #              offsetOrItemOrList, 'itemOrNone', itemOrNone,
@@ -1738,9 +1826,10 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             # offset = item.offset
             # this is equivalent to:
             try:
-                offset = item.getOffsetBySite(item.activeSite)
+                activeSite = item.activeSite
+                offset = item.getOffsetBySite(activeSite)
             except AttributeError:
-                raise StreamException(f'Cannot insert item {item} to stream '
+                raise StreamException(f'Cannot insert item {item!r} to stream '
                                       + '-- is it a music21 object?')
 
         # if not common.isNum(offset):
@@ -1750,7 +1839,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             if offset is None:
                 offset = 0.0
             else:
-                raise StreamException('offset %s must be a number' % offset)
+                raise StreamException(f'Offset {offset!r} must be a number.')
 
         if not isinstance(item, base.Music21Object):
             raise StreamException('to put a non Music21Object in a stream, '
@@ -1794,10 +1883,12 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         {2.0} <music21.note.Rest rest>
         {4.0} <music21.chord.Chord C4 E4>
 
-        Save the original Stream for later
+        Save the original Streams for later
 
         >>> import copy
         >>> s2 = copy.deepcopy(s)
+        >>> s3 = copy.deepcopy(s)
+        >>> s4 = copy.deepcopy(s)
 
         Notice that the duration of the inserted element is not taken into
         consideration and the original element is not broken up,
@@ -1810,24 +1901,87 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         {2.0} <music21.note.Note F#>
         {4.0} <music21.chord.Chord C4 E4 F#4>
 
-        if chordsOnly is set to True then no notes are returned, only chords:
+        if chordsOnly is set to True then no notes are returned, only chords, but
+        untouched notes are left alone:
 
+        >>> s2.insert(5.0, note.Note('E##4'))
         >>> for i in [0.0, 2.0, 4.0]:
         ...     s2.insertIntoNoteOrChord(i, note.Note('F#4'), chordsOnly=True)
         >>> s2.show('text')
         {0.0} <music21.chord.Chord D4 F#4>
         {2.0} <music21.chord.Chord F#4>
         {4.0} <music21.chord.Chord C4 E4 F#4>
+        {5.0} <music21.note.Note E##>
+
+        A chord inserted on top of a note always changes the note into a chord:
+
+        >>> s2.insertIntoNoteOrChord(5.0, chord.Chord('F#4 G-4'))
+        >>> s2.show('text')
+        {0.0} <music21.chord.Chord D4 F#4>
+        {2.0} <music21.chord.Chord F#4>
+        {4.0} <music21.chord.Chord C4 E4 F#4>
+        {5.0} <music21.chord.Chord E##4 F#4 G-4>
+
+        Chords can also be inserted into rests:
+
+        >>> s3.getElementsByOffset(2.0)[0]
+        <music21.note.Rest rest>
+        >>> s3.insertIntoNoteOrChord(2.0, chord.Chord('C4 E4 G#4'))
+        >>> s3.show('text')
+        {0.0} <music21.note.Note D>
+        {2.0} <music21.chord.Chord C4 E4 G#4>
+        {4.0} <music21.chord.Chord C4 E4>
+
+        Despite the variable name, a rest could be inserted into a noteOrChord.
+        It does nothing to existing notes or chords, and just adds a new rest
+        afterwards.
+
+        >>> s4.show('text', addEndTimes=True)
+        {0.0 - 2.0} <music21.note.Note D>
+        {2.0 - 4.0} <music21.note.Rest rest>
+        {4.0 - 5.0} <music21.chord.Chord C4 E4>
+
+        >>> for i in [0.0, 4.0, 6.0]:  # skipping 2.0 for now
+        ...     r = note.Rest(type='quarter')
+        ...     s4.insertIntoNoteOrChord(i, r)
+        >>> r2 = note.Rest(type='quarter')
+        >>> s4.insertIntoNoteOrChord(2.0, r)
+        >>> s4.show('text', addEndTimes=True)
+        {0.0 - 2.0} <music21.note.Note D>
+        {2.0 - 4.0} <music21.note.Rest rest>
+        {4.0 - 5.0} <music21.chord.Chord C4 E4>
+        {6.0 - 7.0} <music21.note.Rest rest>
+
+        Notice that (1) the original duration and not the new duration is used, unless
+        there is no element at that place, and (2) if an element is put into a place where
+        no existing element was found, then it will be found in the new Stream, but if it
+        is placed on top of an existing element, the original element or a new copy will remain:
+
+        >>> r in s4
+        True
+        >>> r2 in s4
+        False
 
+        If a Stream has more than one note, chord, or rest at that position,
+        currently an error is raised.  This may change later:
 
+        >>> s5 = stream.Stream()
+        >>> s5.insert(0, note.Note('C##4'))
+        >>> s5.insert(0, note.Note('E--4'))
+        >>> s5.insertIntoNoteOrChord(0, note.Note('D4'))
+        Traceback (most recent call last):
+        music21.exceptions21.StreamException: more than one element found at the specified offset
         '''
         # could use duration of Note to get end offset span
-        targets = list(self.iter.getElementsByOffset(
-            offset,
-            offset + noteOrChord.quarterLength,  # set end to dur of supplied
-            includeEndBoundary=False,
-            mustFinishInSpan=False,
-            mustBeginInSpan=True).notesAndRests)
+        targets = list(
+            self.iter.getElementsByOffset(
+                offset,
+                offset + noteOrChord.quarterLength,  # set end to dur of supplied
+                includeEndBoundary=False,
+                mustFinishInSpan=False,
+                mustBeginInSpan=True
+            ).notesAndRests
+        )
         removeTarget = None
         # environLocal.printDebug(['insertIntoNoteOrChord', [e for e in targets]])
         if len(targets) == 1:
@@ -1851,6 +2005,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 elif 'Chord' in noteOrChord.classes:
                     pitches = [target.pitch] + list(noteOrChord.pitches)
                     components = [target] + list(noteOrChord)
+                else:
+                    pitches = [target.pitch]
+                    components = [target]
             if 'Chord' in target.classes:
                 # if a chord, make it into a chord
                 if 'Note' in noteOrChord.classes:
@@ -1859,6 +2016,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 elif 'Chord' in noteOrChord.classes:
                     pitches = list(target.pitches) + list(noteOrChord.pitches)
                     components = list(target) + list(noteOrChord)
+                else:
+                    pitches = list(target.pitches)
+                    components = list(target)
 
             if len(pitches) > 1 or chordsOnly is True:
                 finalTarget = chord.Chord(pitches)
@@ -1926,8 +2086,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         (6.0, 9.0)
         >>> notes2 = []
 
-        Since notes are not embedded in Elements here, their offset
-        changes when they are added to a stream!
+        Notes' naive offsets will
+        change when they are added to a stream.
 
         >>> for x in range(3):
         ...     n = note.Note('A-')
@@ -2022,7 +2182,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         # we cannot keep the index cache here b/c we might
         self.coreElementsChanged(updateIsFlat=updateIsFlat)
         self.isSorted = storeSorted
-        self._setHighestTime(highestTime)  # call after to store in cache
+        self._setHighestTime(opFrac(highestTime))  # call after to store in cache
 
     def storeAtEnd(self, itemOrList, ignoreSort=False):
         '''
@@ -2072,7 +2232,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         #     element = item
 
         # cannot support elements with Durations in the highest time list
-        if element.duration is not None and element.duration.quarterLength != 0:
+        if element.duration.quarterLength != 0:
             raise StreamException('cannot insert an object with a non-zero '
                                   + 'Duration into the highest time elements list')
 
@@ -2193,12 +2353,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         '''
         # need to find the highest time after the insert
         if itemOrNone is not None:  # we have an offset and an element
-            # if hasattr(itemOrNone, 'duration') and itemOrNone.duration is not None:
             insertObject = itemOrNone
-            if insertObject.duration is not None:
-                qL = insertObject.duration.quarterLength
-            else:
-                qL = 0.0
+            qL = insertObject.duration.quarterLength
             offset = offsetOrItemOrList
             lowestOffsetInsert = offset
             highestTimeInsert = offset + qL
@@ -2211,11 +2367,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             while i < len(insertList):
                 o = insertList[i]
                 e = insertList[i + 1]
-                # if hasattr(e, 'duration')  and e.duration is not None:
-                if e.duration is not None:
-                    qL = e.duration.quarterLength
-                else:
-                    qL = 0.0
+                qL = e.duration.quarterLength
                 if o + qL > highestTimeInsert:
                     highestTimeInsert = o + qL
                 if lowestOffsetInsert is None or o < lowestOffsetInsert:
@@ -2224,10 +2376,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         else:  # using native offset
             # if hasattr(offsetOrItemOrList, 'duration'):
             insertObject = offsetOrItemOrList
-            if insertObject.duration is not None:
-                qL = insertObject.duration.quarterLength
-            else:
-                qL = 0.0
+            qL = insertObject.duration.quarterLength
             # should this be getOffsetBySite(None)?
             highestTimeInsert = insertObject.offset + qL
             lowestOffsetInsert = insertObject.offset
@@ -2913,7 +3062,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         '''
         return self.iter.getElementsByGroup(groupFilterList).stream()
 
-    def getElementById(self, elementId, classFilter=None):
+    def getElementById(self, elementId, classFilter=None) -> Optional[base.Music21Object]:
         '''
         Returns the first encountered element for a given id. Return None
         if no match. Note: this uses the id attribute stored on elements,
@@ -2943,8 +3092,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         True
         >>> ew.activeSite is a
         True
-
-        :rtype: base.Music21Object
         '''
         sIterator = self.iter.addFilter(filters.IdFilter(elementId))
         if classFilter is not None:
@@ -4275,17 +4422,10 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
     # --------------------------------------------------------------------------
     # handling transposition values and status
-    def _getAtSoundingPitch(self):
-        return self._atSoundingPitch
 
-    def _setAtSoundingPitch(self, value):
-        if value in [True, False, 'unknown']:
-            self._atSoundingPitch = value
-        else:
-            raise StreamException('not a valid at sounding pitch value: %s' %
-                                  value)
-
-    atSoundingPitch = property(_getAtSoundingPitch, _setAtSoundingPitch, doc='''
+    @property
+    def atSoundingPitch(self) -> Union[bool, str]:
+        '''
         Get or set the atSoundingPitch status, that is whether the
         score is at concert pitch or may have transposing instruments
         that will not sound as notated.
@@ -4304,7 +4444,17 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> s.atSoundingPitch = 'junk'
         Traceback (most recent call last):
         music21.exceptions21.StreamException: not a valid at sounding pitch value: junk
-        ''')
+        '''
+        return self._atSoundingPitch
+
+    @atSoundingPitch.setter
+    def atSoundingPitch(self, value: Union[bool, str]):
+        if value in [True, False, 'unknown']:
+            self._atSoundingPitch = value
+        else:
+            raise StreamException('not a valid at sounding pitch value: %s' %
+                                  value)
+
 
     def _transposeByInstrument(self,
                                reverse=False,
@@ -5168,14 +5318,25 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         additional objects by placing class names in the `collect` list.
         By default, TimeSignature and KeySignature objects are collected.
         '''
-        def dealWithSubNotes(chordLength, subNotes, returnObj):
-            # environLocal.printDebug(['creating chord from subNotes',
-            #   subNotes, 'inPlace', inPlace])
+        if not inPlace:  # make a copy
+            # since we do not return Scores, this probably should always be
+            # a Stream
+            # returnObj = Stream()
+            # returnObj = self.__class__()  # for output
+            returnObj = copy.deepcopy(self)
+            returnObj.derivation.origin = self
+            returnObj.derivation.method = 'makeChords'
+        else:
+            returnObj = self
+
+        def dealWithSubNotes(chordLength, noteList):
+            # environLocal.printDebug(['creating chord from noteList',
+            #   noteList, 'inPlace', inPlace])
             c = chord.Chord()
             c.duration.quarterLength = chordLength
             # these are references, not copies, for now
             tempComponents = []
-            for n in subNotes:
+            for n in noteList:
                 if n.isChord:
                     cSub = list(n)
                 else:
@@ -5195,13 +5356,13 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                     c.setTie(comp.tie.type, comp.pitch)
 
             if gatherArticulations:
-                for n in subNotes:
+                for n in noteList:
                     c.articulations += n.articulations
             if gatherExpressions:
-                for n in subNotes:
+                for n in noteList:
                     c.expressions += n.expressions
             # always remove all the previous elements
-            for n in subNotes:
+            for n in noteList:
                 returnObj.remove(n)
             # remove all rests found in source
             for r in list(returnObj.getElementsByClass('Rest')):
@@ -5220,17 +5381,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         # environLocal.printDebug(['makeChords():',
         #              'transferGroupsToPitches', transferGroupsToPitches])
 
-        if not inPlace:  # make a copy
-            # since we do not return Scores, this probably should always be
-            # a Stream
-            # returnObj = Stream()
-            # returnObj = self.__class__()  # for output
-            returnObj = copy.deepcopy(self)
-            returnObj.derivation.origin = self
-            returnObj.derivation.method = 'makeChords'
-        else:
-            returnObj = self
-
         if returnObj.hasMeasures():
             # call on component measures
             for m in returnObj.getElementsByClass('Measure'):
@@ -5279,13 +5429,13 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 # get all notes within the start and the min window size
                 oStart = o
                 oEnd = oStart + minimumWindowSize
-                subNotes = returnObj.getElementsByOffset(
+                subNotes = list(returnObj.getElementsByOffset(
                     oStart,
                     oEnd,
                     includeEndBoundary=False,
                     mustFinishInSpan=False,
                     mustBeginInSpan=True
-                ).getElementsByClass(matchClasses).stream()  # get once for speed
+                ).getElementsByClass(matchClasses))  # get once for speed
                 # environLocal.printDebug(['subNotes', subNotes])
                 qlMax = None
                 # get the max duration found from within the window
@@ -5302,21 +5452,21 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 # oStart + minimumWindowSize and oStart + qlMax
                 if (includePostWindow and qlMax is not None
                         and qlMax > minimumWindowSize):
-                    subAdd = returnObj.getElementsByOffset(
+                    subAdd = list(returnObj.getElementsByOffset(
                         oStart + minimumWindowSize,
                         oStart + qlMax,
                         includeEndBoundary=False,
                         mustFinishInSpan=False,
                         mustBeginInSpan=True
-                    ).getElementsByClass(matchClasses).stream()
+                    ).getElementsByClass(matchClasses))
                     # concatenate any additional notes found
                     subNotes += subAdd
 
                 # make subNotes into a chord
                 if subNotes:
-                    c = dealWithSubNotes(qlMax, subNotes, returnObj)
+                    cOut = dealWithSubNotes(qlMax, subNotes)
                     # insert chord at start location
-                    returnObj.coreInsert(o, c)
+                    returnObj.coreInsert(o, cOut)
                 # environLocal.printDebug(['len of returnObj', len(returnObj)])
                 # shift offset to qlMax or minimumWindowSize
                 if qlMax is not None and qlMax >= minimumWindowSize:
@@ -5338,21 +5488,21 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 # get all notes within the start and the min window size
                 oStart = onAndOffOffsets[i]
                 oEnd = onAndOffOffsets[i + 1]
-                subNotes = returnObj.getElementsByOffset(
+                subNotes = list(returnObj.getElementsByOffset(
                     oStart,
                     oEnd,
                     includeEndBoundary=False,
                     mustFinishInSpan=False,
                     mustBeginInSpan=True
-                ).getElementsByClass(matchClasses).stream()
+                ).getElementsByClass(matchClasses))
                 # environLocal.printDebug(['subNotes', subNotes])
                 # subNotes.show('t')
 
                 # make subNotes into a chord
                 if subNotes:
-                    c = dealWithSubNotes(oEnd - oStart, subNotes, returnObj)
+                    cOut = dealWithSubNotes(oEnd - oStart, subNotes)
                     # insert chord at start location
-                    returnObj.coreInsert(oStart, c)
+                    returnObj.coreInsert(oStart, cOut)
 
         # makeRests to fill any gaps produced by stripping
         # environLocal.printDebug(['pre makeRests show()'])
@@ -5363,13 +5513,16 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         returnObj.coreElementsChanged()
         return returnObj
 
-    def chordify(self, *,
-                 addTies=True,
-                 addPartIdAsGroup=False,
-                 removeRedundantPitches=True,
-                 toSoundingPitch=True,
-                 copyPitches=True,
-                 ):
+    def chordify(
+        self,
+        *,
+        addTies=True,
+        addPartIdAsGroup=False,
+        removeRedundantPitches=True,
+        toSoundingPitch=True,
+        copyPitches=True,
+    ):
+        # noinspection PyShadowingNames
         '''
         Create a chordal reduction of polyphonic music, where each
         change to a new pitch results in a new chord. If a Score or
@@ -5544,7 +5697,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> cn[0].pitches
         (<music21.pitch.Pitch C4>, <music21.pitch.Pitch D#4>)
         '''
-        def chordifyOneMeasure(template, streamToChordify):
+        def chordifyOneMeasure(templateInner, streamToChordify):
             timespanTree = streamToChordify.asTimespans(classList=('GeneralNote',))
             allTimePoints = timespanTree.allTimePoints()
             if 0 not in allTimePoints:
@@ -5558,7 +5711,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 if quarterLength < 0:
                     environLocal.warn(
                         'Something is wrong with the verticality '
-                        + f'in stream {template!r}: {vert!r} '
+                        + f'in stream {templateInner!r}: {vert!r} '
                         + f'its endTime {endTime} is less than its offset {offset}'
                     )
 
@@ -5569,30 +5722,30 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                                                copyPitches=copyPitches,
                                                )
 
-                template.coreInsert(opFrac(offset), chordOrRest)
-            template.coreElementsChanged()
-            consolidateRests(template)
+                templateInner.coreInsert(opFrac(offset), chordOrRest)
+            templateInner.coreElementsChanged()
+            consolidateRests(templateInner)
 
-        def consolidateRests(template):
+        def consolidateRests(templateInner):
             consecutiveRests = []
-            for el in list(template.getElementsByClass('GeneralNote')):
+            for el in list(templateInner.getElementsByClass('GeneralNote')):
                 if 'Rest' not in el.classes:
-                    removeConsecutiveRests(template, consecutiveRests)
+                    removeConsecutiveRests(templateInner, consecutiveRests)
                     consecutiveRests = []
                 else:
                     consecutiveRests.append(el)
-            removeConsecutiveRests(template, consecutiveRests)
+            removeConsecutiveRests(templateInner, consecutiveRests)
 
-        def removeConsecutiveRests(template, consecutiveRests):
+        def removeConsecutiveRests(templateInner, consecutiveRests):
             if len(consecutiveRests) < 2:
                 return
             totalDuration = sum(r.duration.quarterLength for r in consecutiveRests)
-            startOffset = template.elementOffset(consecutiveRests[0])
+            startOffset = templateInner.elementOffset(consecutiveRests[0])
             for r in consecutiveRests:
-                template.remove(r)
+                templateInner.remove(r)
             rNew = note.Rest()
             rNew.duration.quarterLength = totalDuration
-            template.insert(startOffset, rNew)
+            templateInner.insert(startOffset, rNew)
 
         # --------------------------------------
         if toSoundingPitch:
@@ -5756,6 +5909,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             groups = []
             for i, v in enumerate(srcObj.voices):
                 groups.append((v.flat, i))
+            elsNotOfVoice = srcObj.getElementsNotOfClass('Voice')
+            if len(elsNotOfVoice) > 0:
+                groups.insert(0, (elsNotOfVoice, None))
         else:  # create a single collection
             groups = [(srcObj, None)]
         # environLocal.printDebug(['offsetMap', groups])
@@ -5764,11 +5920,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 # do not include barlines
                 if isinstance(e, bar.Barline):
                     continue
-                # if hasattr(e, 'duration') and e.duration is not None:
-                if e.duration is not None:
-                    dur = e.duration.quarterLength
-                else:
-                    dur = 0
+                dur = e.duration.quarterLength
                 offset = group.elementOffset(e)
                 endTime = opFrac(offset + dur)
                 # NOTE: used to make a copy.copy of elements here;
@@ -6350,23 +6502,23 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         posConnected = []  # temporary storage for index of tied notes
         posDelete = []  # store deletions to be processed later
 
-        def updateEndMatch(n):
+        def updateEndMatch(nInner):
             '''
             updateEndMatch based on nList, iLast, matchByPitch, etc.
             '''
             # ties tell us when they are ended
             # unless n is a chord, which only tells if SOME member has a tie
             # https://github.com/cuthbertLab/music21/issues/502
-            if (hasattr(n, 'tie')
-                    and 'Chord' not in n.classes
-                    and n.tie is not None
-                    and n.tie.type == 'stop'):
+            if (hasattr(nInner, 'tie')
+                    and 'Chord' not in nInner.classes
+                    and nInner.tie is not None
+                    and nInner.tie.type == 'stop'):
                 return True
             # but capture case where all chord members have a stop tie
-            elif (hasattr(n, 'tie')
-                    and 'Chord' in n.classes
-                    and None not in [inner_p.tie for inner_p in n.notes]
-                    and {inner_p.tie.type for inner_p in n.notes} == {'stop'}):
+            elif (hasattr(nInner, 'tie')
+                    and 'Chord' in nInner.classes
+                    and None not in [inner_p.tie for inner_p in nInner.notes]
+                    and {inner_p.tie.type for inner_p in nInner.notes} == {'stop'}):
                 return True
             # if we cannot find a stop tie, see if last note was connected
             # and this and the last note are the same pitch; this assumes
@@ -6381,21 +6533,21 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             if (nLast is not None
                     and iLast in posConnected
                     and hasattr(nLast, 'pitch')
-                    and hasattr(n, 'pitch')
-                    and nLast.pitch == n.pitch):
+                    and hasattr(nInner, 'pitch')
+                    and nLast.pitch == nInner.pitch):
                 return True
             # looking for two chords of equal size
             elif (nLast is not None
-                    and 'Note' not in n.classes
+                    and 'Note' not in nInner.classes
                     and iLast in posConnected
                     and hasattr(nLast, 'pitches')
-                    and hasattr(n, 'pitches')):
-                if len(nLast.pitches) != len(n.pitches):
+                    and hasattr(nInner, 'pitches')):
+                if len(nLast.pitches) != len(nInner.pitches):
                     return False
 
                 allPitchesMatched = True
                 for pitchIndex in range(len(nLast.pitches)):
-                    if nLast.pitches[pitchIndex] != n.pitches[pitchIndex]:
+                    if nLast.pitches[pitchIndex] != nInner.pitches[pitchIndex]:
                         allPitchesMatched = False
                         break
                 return allPitchesMatched
@@ -6508,8 +6660,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             if ignoreRests:
                 # need to find the offset of the first thing that is not rest
                 for j in range(currentIndex + 1, len(srcStream._elements)):
-                    e = srcStream._elements[j]
-                    if 'NotRest' in e.classes:
+                    el = srcStream._elements[j]
+                    if 'NotRest' in el.classes:
                         # change target offset to this position
                         targetOffset = srcStream._elements[j].getOffsetBySite(
                             srcStream)
@@ -6517,9 +6669,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             match = srcStream.getElementsByOffset(targetOffset)
             # filter matched elements
             post = []
-            for m in match:
-                if 'NotRest' in m.classes:
-                    post.append(m)
+            for matchEl in match:
+                if 'NotRest' in matchEl.classes:
+                    post.append(matchEl)
             return post
 
         # take all flat elements; this will remove all voices; just use offset
@@ -7258,6 +7410,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         47.0
         >>> r = q.flat
 
+        Changed in v6.5 -- highestTime can return a Fraction.
+
+
         OMIT_FROM_DOCS
 
         Make sure that the cache really is empty
@@ -7265,6 +7420,19 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         False
         >>> r.highestTime  # 44 + 3
         47.0
+
+        Can highestTime be a fraction?
+
+        >>> n = note.Note('D')
+        >>> n.duration.quarterLength = 1/3
+        >>> n.duration.quarterLength
+        Fraction(1, 3)
+
+        >>> s = stream.Stream()
+        >>> s.append(note.Note('C'))
+        >>> s.append(n)
+        >>> s.highestTime
+        Fraction(4, 3)
         '''
         # TODO(msc) -- why is cache 'HighestTime' and not 'highestTime'?
         # environLocal.printDebug(['_getHighestTime', 'isSorted', self.isSorted, self])
@@ -7276,7 +7444,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             self._cache['HighestTime'] = 0.0
             return 0.0
         else:
-            highestTimeSoFar = Fraction(0, 1)
+            highestTimeSoFar = 0.0
             # TODO: optimize for a faster way of doing this.
             # but cannot simply look at the last element because what if the penultimate
             # element, with a
@@ -7288,7 +7456,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                                    + e.duration.quarterLength)
                 if candidateOffset > highestTimeSoFar:
                     highestTimeSoFar = candidateOffset
-            self._cache['HighestTime'] = float(highestTimeSoFar)
+            self._cache['HighestTime'] = opFrac(highestTimeSoFar)
         return self._cache['HighestTime']
 
     @property
@@ -7651,10 +7819,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             for e in group:
                 if isinstance(e, bar.Barline):
                     continue
-                if e.duration is not None:
-                    dur = e.duration.quarterLength
-                else:
-                    dur = 0
+                dur = e.duration.quarterLength
                 offset = round(e.getOffsetBySite(group), 8)
                 # calculate all time regions given this offset
 
@@ -7735,8 +7900,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         Get or set the :class:`~music21.metadata.Metadata` object
         found at the beginning (offset 0) of this Stream.
 
-
-
         >>> s = stream.Stream()
         >>> s.metadata = metadata.Metadata()
         >>> s.metadata.composer = 'frank'
@@ -8090,8 +8253,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             returnObj = self
 
         for e in returnObj.recurse().getElementsNotOfClass('Stream'):
-            if e.duration is not None:
-                e.duration = e.duration.augmentOrDiminish(amountToScale)
+            e.duration = e.duration.augmentOrDiminish(amountToScale)
 
         returnObj.coreElementsChanged()
         if inPlace is not True:
@@ -8258,8 +8420,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         def bestMatch(target, divisors):
             found = []
             for div in divisors:
-                match, error, signedError = common.nearestMultiple(target, (1 / div))
-                found.append((error, match, signedError))  # reverse for sorting
+                match, error, signedErrorInner = common.nearestMultiple(target, (1 / div))
+                found.append((error, match, signedErrorInner))  # reverse match, error for sorting
             # get first, and leave out the error
             bestMatchTuple = sorted(found)[0]
             return bestMatchTuple
@@ -8296,20 +8458,19 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                             and signedError != 0):
                         e.editorial.offsetQuantizationError = signedError * sign
                 if processDurations:
-                    if e.duration is not None:
-                        ql = e.duration.quarterLength
-                        if ql < 0:  # buggy MIDI file?
-                            ql = 0
-                        unused_error, qlNew, signedError = bestMatch(
-                            float(ql), quarterLengthDivisors)
-                        # Enforce nonzero duration for non-grace notes
-                        if qlNew == 0 and not e.duration.isGrace:
-                            qlNew = 1 / max(quarterLengthDivisors)
-                            signedError = ql - qlNew
-                        e.duration.quarterLength = qlNew
-                        if (hasattr(e, 'editorial')
-                                and signedError != 0):
-                            e.editorial.quarterLengthQuantizationError = signedError
+                    ql = e.duration.quarterLength
+                    if ql < 0:  # buggy MIDI file?
+                        ql = 0
+                    unused_error, qlNew, signedError = bestMatch(
+                        float(ql), quarterLengthDivisors)
+                    # Enforce nonzero duration for non-grace notes
+                    if qlNew == 0 and 'GeneralNote' in e.classes and not e.duration.isGrace:
+                        qlNew = 1 / max(quarterLengthDivisors)
+                        signedError = ql - qlNew
+                    e.duration.quarterLength = qlNew
+                    if (hasattr(e, 'editorial')
+                            and signedError != 0):
+                        e.editorial.quarterLengthQuantizationError = signedError
 
         if inPlace is False:
             return returnStream
@@ -8484,12 +8645,15 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         if not inPlace:
             return returnObj
 
-    def sliceAtOffsets(self,
-                       offsetList,
-                       target=None,
-                       addTies=True,
-                       inPlace=False,
-                       displayTiedAccidentals=False):
+    def sliceAtOffsets(
+        self,
+        offsetList,
+        target=None,
+        addTies=True,
+        inPlace=False,
+        displayTiedAccidentals=False
+    ):
+        # noinspection PyShadowingNames
         '''
         Given a list of quarter lengths, slice and optionally tie all
         Music21Objects crossing these points.
@@ -9238,11 +9402,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         '''
         post = []
         for e in flatStream:
-            if e.duration is None:
-                durSpan = (e.offset, e.offset)
-            else:
-                dur = e.duration.quarterLength
-                durSpan = (e.offset, opFrac(e.offset + dur))
+            dur = e.duration.quarterLength
+            durSpan = (e.offset, opFrac(e.offset + dur))
             post.append(durSpan)
         # assume this is already sorted
         # index found here will be the same as elementsSorted
@@ -9422,10 +9583,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 gapElement.duration = duration.Duration()
                 gapElement.duration.quarterLength = gapQuarterLength
                 gapStream.insert(highestCurrentEndTime, gapElement, ignoreSort=True)
-            if hasattr(e, 'duration') and e.duration is not None:
-                eDur = e.duration.quarterLength
-            else:
-                eDur = 0.
+            eDur = e.duration.quarterLength
             highestCurrentEndTime = opFrac(max(highestCurrentEndTime, e.offset + eDur))
 
         # TODO: Is this even necessary, we do insert the elements in sorted order
@@ -10172,50 +10330,54 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 p.id = str(self.id) + '-' + voiceId
                 partDict[voiceId] = p
 
-        def doOneMeasureWithVoices(m):
+        def doOneMeasureWithVoices(mInner):
             '''
             This is the main routine for dealing with the most common
             and most difficult voice set.
             '''
             mActive = Measure()
-            mActive.mergeAttributes(m)  # get groups, optional id
+            mActive.mergeAttributes(mInner)  # get groups, optional id
             # merge everything except Voices; this will get
             # clefs
-            mActive.mergeElements(m, classFilterList=(
-                'Barline', 'TimeSignature', 'Clef', 'KeySignature'))
+            mActive.mergeElements(
+                mInner,
+                classFilterList=(
+                    'Barline', 'TimeSignature', 'Clef', 'KeySignature',
+                )
+            )
 
-            # vIndex = 0 should not be necessary, but pylint warns on loop variables
+            # vIndexInner = 0 should not be necessary, but pylint warns on loop variables
             # that could possibly be undefined used out of the loop.
-            vIndex = 0
+            vIndexInner = 0
 
             seenIdsThisMeasure = set()
-            for vIndex, v in enumerate(m.voices):
+            for vIndexInner, vInner in enumerate(mInner.voices):
                 # TODO(msc): fix bugs if same voice id appears twice in same measure
 
                 # make an independent copy
-                mNew = copy.deepcopy(mActive)
+                mNewInner = copy.deepcopy(mActive)
                 # merge all elements from the voice
-                mNew.mergeElements(v)
+                mNewInner.mergeElements(vInner)
                 # insert in the appropriate part
-                vId = v.id
+                vId = vInner.id
                 if not separateById:
-                    p = partDict[vIndex]
+                    pInner = partDict[vIndexInner]
                 else:
                     seenIdsThisMeasure.add(vId)
-                    p = partDict[vId]
-                p.insert(self.elementOffset(m), mNew)
+                    pInner = partDict[vId]
+                pInner.insert(self.elementOffset(mInner), mNewInner)
 
-            # vIndex is now the number of voices - 1.  Fill empty voices
+            # vIndexInner is now the number of voices - 1.  Fill empty voices
             if not separateById:
-                for emptyIndex in range(vIndex + 1, partCount):
-                    p = partDict[emptyIndex]
-                    p.insert(self.elementOffset(m), copy.deepcopy(mActive))
+                for emptyIndex in range(vIndexInner + 1, partCount):
+                    pInner = partDict[emptyIndex]
+                    pInner.insert(self.elementOffset(mInner), copy.deepcopy(mActive))
             else:
-                for voiceId in partDict:
-                    if voiceId in seenIdsThisMeasure:
+                for voiceIdInner in partDict:
+                    if voiceIdInner in seenIdsThisMeasure:
                         continue
-                    p = partDict[voiceId]
-                    p.insert(self.elementOffset(m), copy.deepcopy(mActive))
+                    pInner = partDict[voiceIdInner]
+                    pInner.insert(self.elementOffset(mInner), copy.deepcopy(mActive))
 
         if self.hasMeasures():
             for m in self.iter.getElementsByClass('Measure'):
@@ -11178,14 +11340,11 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                                                        includeEndBoundary=True,
                                                        mustFinishInSpan=False,
                                                        mustBeginInSpan=True,
-                                                       classList=['Measure'])
-            # I think this may be a mistake -- and empty will still be True...
-            if measuresToCheck != []:
-                for m in measuresToCheck:
-                    if highestMeasure is None or m.number > highestMeasure:
-                        highestMeasure = m.number
-            else:
-                highestMeasure = 0
+                                                       ).getElementsByClass('Measure')
+            highestMeasure = 0
+            for m in measuresToCheck:
+                if highestMeasure is None or m.number > highestMeasure:
+                    highestMeasure = m.number
 
         # remove the source variant
         self.remove(v)
diff --git a/music21/stream/core.py b/music21/stream/core.py
index 6ee50efe2..2219f556a 100644
--- a/music21/stream/core.py
+++ b/music21/stream/core.py
@@ -22,35 +22,47 @@ remain stable.
 All functions here will eventually begin with `.core`.
 '''
 # pylint: disable=attribute-defined-outside-init
-
+from typing import List, Dict, Union, Tuple
+from fractions import Fraction
 import unittest
 
+from music21.base import Music21Object
 from music21 import spanner
 from music21 import tree
 from music21.exceptions21 import StreamException, ImmutableStreamException
 
+OFFSET_STRING_VALUES = {'highestTime', 'lowestOffset', 'highestOffset'}
 
 class StreamCoreMixin:
     def __init__(self):
         # hugely important -- keeps track of where the _elements are
         # the _offsetDict is a dictionary where id(element) is the
-        # index and the index and the offset is the value.
-        self._offsetDict = {}
+        # index and the value is a tuple of offset and element.
+        # offsets can be floats, Fractions, or the special string 'highestTime'
+        self._offsetDict: Dict[int, Tuple[Union[float, Fraction, str], Music21Object]] = {}
+
         # self._elements stores Music21Object objects.
-        self._elements = []
+        self._elements: List[Music21Object] = []
 
         # self._endElements stores Music21Objects found at
         # the highestTime of this Stream.
-        self._endElements = []
+        self._endElements: List[Music21Object] = []
 
         self.isSorted = True
-        # v4!
+        # should isFlat become readonly?
+        self.isFlat = True  # does it have no embedded Streams
+
+        # someday...
         # self._elementTree = tree.trees.ElementTree(source=self)
 
-    def coreInsert(self, offset, element,
-                   *,
-                   ignoreSort=False, setActiveSite=True
-                   ):
+    def coreInsert(
+        self,
+        offset: Union[float, Fraction],
+        element: Music21Object,
+        *,
+        ignoreSort=False,
+        setActiveSite=True
+    ):
         '''
         N.B. -- a "core" method, not to be used by general users.  Run .insert() instead.
 
@@ -93,7 +105,12 @@ class StreamCoreMixin:
                         if highestSortTuple < thisSortTuple:
                             storeSorted = True
 
-        self.setElementOffset(element, float(offset), addElement=True, setActiveSite=setActiveSite)
+        self.setElementOffset(
+            element,
+            float(offset),  # why is this not opFrac?
+            addElement=True,
+            setActiveSite=setActiveSite
+        )
         element.sites.add(self)
         # need to explicitly set the activeSite of the element
         # will be sorted later if necessary
@@ -101,7 +118,12 @@ class StreamCoreMixin:
         # self._elementTree.insert(float(offset), element)
         return storeSorted
 
-    def coreAppend(self, element, setActiveSite=True):
+    def coreAppend(
+        self,
+        element: Music21Object,
+        *,
+        setActiveSite=True
+    ):
         '''
         N.B. -- a "core" method, not to be used by general users.  Run .append() instead.
 
@@ -124,19 +146,19 @@ class StreamCoreMixin:
         # Make this faster
         # self._elementTree.insert(self.highestTime, element)
         # does not change sorted state
-        if element.duration is not None:
-            self._setHighestTime(ht + element.duration.quarterLength)
+        self._setHighestTime(ht + element.duration.quarterLength)
     # --------------------------------------------------------------------------
     # adding and editing Elements and Streams -- all need to call coreElementsChanged
     # most will set isSorted to False
 
     def coreElementsChanged(
-            self,
-            *,
-            updateIsFlat=True,
-            clearIsSorted=True,
-            memo=None,
-            keepIndex=False):
+        self,
+        *,
+        updateIsFlat=True,
+        clearIsSorted=True,
+        memo=None,
+        keepIndex=False
+    ):
         '''
         NB -- a "core" stream method that is not necessary for most users.
 
@@ -222,7 +244,7 @@ class StreamCoreMixin:
             if keepIndex and indexCache is not None:
                 self._cache['index'] = indexCache
 
-    def coreHasElementByMemoryLocation(self, objId):
+    def coreHasElementByMemoryLocation(self, objId: int) -> bool:
         '''
         NB -- a "core" stream method that is not necessary for most users. use hasElement(obj)
 
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index 6dc0e9b75..0debf51d9 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -1114,9 +1114,6 @@ def makeTies(s,
                 # environLocal.printDebug([
                 #    'Stream.makeTies() iterating over elements in measure',
                 #    m, e])
-                # if hasattr(e, 'duration') and e.duration is not None:
-                if e.duration is None:
-                    continue
                 # check to see if duration is within Measure
                 eOffset = v.elementOffset(e)
                 eEnd = opFrac(eOffset + e.duration.quarterLength)
@@ -1127,9 +1124,9 @@ def makeTies(s,
                     continue
                 if eOffset >= mEnd:
                     continue  # skip elements that extend past measure boundary.
-#                             raise stream.StreamException(
-#                                 'element (%s) has offset %s within a measure '
-#                                 'that ends at offset %s' % (e, eOffset, mEnd))
+                    # raise stream.StreamException(
+                    #     'element (%s) has offset %s within a measure '
+                    #     'that ends at offset %s' % (e, eOffset, mEnd))
 
                 qLenBegin = mEnd - eOffset
                 e, eRemain = e.splitAtQuarterLength(qLenBegin,


Test patch:
diff --git a/music21/musicxml/testPrimitive.py b/music21/musicxml/testPrimitive.py
index 66ec96f6c..a1fa4dc7d 100644
--- a/music21/musicxml/testPrimitive.py
+++ b/music21/musicxml/testPrimitive.py
@@ -17932,6 +17932,146 @@ multiDigitEnding = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 </score-partwise>
 '''
 
+
+tupletsImplied = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
+<score-partwise version="3.1">
+  <part-list>
+    <score-part id="P1">
+      <part-name print-object="no">MusicXML Part</part-name>
+      <score-instrument id="P1-I1">
+        <instrument-name>None</instrument-name>
+      </score-instrument>
+    </score-part>
+  </part-list>
+  <part id="P1">
+    <measure number="1" width="983">
+      <attributes>
+        <divisions>6</divisions>
+        <key>
+          <fifths>0</fifths>
+          <mode>major</mode>
+        </key>
+        <time>
+          <beats>2</beats>
+          <beat-type>4</beat-type>
+        </time>
+        <clef>
+          <sign>G</sign>
+          <line>2</line>
+        </clef>
+      </attributes>
+      <note default-x="83">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet number="1" placement="below" type="start"/>
+        </notations>
+      </note>
+      <note default-x="232">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">continue</beam>
+      </note>
+      <note default-x="380">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet number="1" type="stop"/>
+        </notations>
+      </note>
+      <note default-x="528">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">begin</beam>
+        <notations>
+          <tuplet number="1" placement="below" show-number="none" type="start"/>
+        </notations>
+      </note>
+      <note default-x="677">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">continue</beam>
+      </note>
+      <note default-x="825">
+        <pitch>
+          <step>C</step>
+          <octave>5</octave>
+        </pitch>
+        <duration>2</duration>
+        <voice>1</voice>
+        <type>eighth</type>
+        <time-modification>
+          <actual-notes>3</actual-notes>
+          <normal-notes>2</normal-notes>
+        </time-modification>
+        <stem default-y="-50">down</stem>
+        <beam number="1">end</beam>
+        <notations>
+          <tuplet number="1" type="stop"/>
+        </notations>
+      </note>
+      <barline location="right">
+        <bar-style>light-heavy</bar-style>
+      </barline>
+    </measure>
+  </part>
+</score-partwise>
+'''
+
+
 ALL = [
     articulations01, pitches01a, directions31a, lyricsMelisma61d, notations32a,  # 0
     restsDurations02a, rhythmDurations03a, chordsThreeNotesDuration21c,  # 5
@@ -17947,7 +18087,7 @@ ALL = [
     mixedVoices1a, mixedVoices1b, mixedVoices2,  # 37
     colors01, triplets01, textBoxes01, octaveShifts33d,  # 40
     unicodeStrNoNonAscii, unicodeStrWithNonAscii,  # 44
-    tremoloTest, hiddenRests, multiDigitEnding  # 46
+    tremoloTest, hiddenRests, multiDigitEnding, tupletsImplied  # 46
 ]
 
 
diff --git a/music21/test/coverageM21.py b/music21/test/coverageM21.py
index 138329114..55c00390b 100644
--- a/music21/test/coverageM21.py
+++ b/music21/test/coverageM21.py
@@ -34,8 +34,8 @@ exclude_lines = [
 
 
 def getCoverage(overrideVersion=False):
-    if overrideVersion or sys.version_info.minor == 6:
-        # run on Py 3.6 -- to get Py 3.5/3.7 timing...
+    if overrideVersion or sys.version_info.minor == 7:
+        # run on Py 3.7 -- to get Py 3.8/3.9 timing...
         try:
             import coverage
             cov = coverage.Coverage(omit=omit_modules)
diff --git a/music21/test/testSingleCoreAll.py b/music21/test/testSingleCoreAll.py
index 9b6ae5300..30f31d011 100644
--- a/music21/test/testSingleCoreAll.py
+++ b/music21/test/testSingleCoreAll.py
@@ -119,8 +119,8 @@ def main(testGroup=('test',), restoreEnvironmentDefaults=False, limit=None, verb
     return returnCode
 
 
-def travisMain():
-    # the main call for travis-ci tests.
+def ciMain():
+    # the main call for ci tests.
     # exits with the returnCode
     returnCode = main(verbosity=1)
     sys.exit(returnCode)
diff --git a/music21/test/testStream.py b/music21/test/testStream.py
index b468682d0..659f1c174 100644
--- a/music21/test/testStream.py
+++ b/music21/test/testStream.py
@@ -167,24 +167,31 @@ class TestExternal(unittest.TestCase):  # pragma: no cover
         s.show()
 
     def testMultipartMeasures(self):
-        '''This demonstrates obtaining slices from a stream and layering
+        '''
+        This demonstrates obtaining slices from a stream and layering
         them into individual parts.
-
-        OMIT_FROM_DOCS
-        TODO: this should show instruments
-        this is presently not showing instruments
-        probably b/c when appending to s Stream activeSite is set to that stream
         '''
-        from music21 import corpus, converter
-        a = converter.parse(corpus.getWork(['mozart', 'k155', 'movement2.xml']))
-        b = a[8][4:8]
-        c = a[8][8:12]
-        d = a[8][12:16]
+        from music21 import corpus
+        k155_score = corpus.parse('mozart/k155', 2)
+        start_measure = 0
+        end_measure = 16
+
+        violin_i_id = k155_score.parts[0].id
+        violin_i_part = k155_score[violin_i_id]
+        violin_i_part_slice = violin_i_part[start_measure:end_measure + 1]
+
+        violin_ii_id = k155_score.parts[1].id
+        violin_ii_part = k155_score[violin_ii_id]
+        violin_ii_part_slice = violin_ii_part[start_measure:end_measure + 1]
+
+        viola_part_id = k155_score.parts[2].id
+        viola_part = k155_score[viola_part_id]
+        viola_part_slice = viola_part[start_measure:end_measure + 1]
 
         s = Stream()
-        s.insert(b)
-        s.insert(c)
-        s.insert(d)
+        s.append(violin_i_part_slice)
+        s.append(violin_ii_part_slice)
+        s.append(viola_part_slice)
         s.show()
 
     def testCanons(self):
@@ -3893,7 +3900,9 @@ class Test(unittest.TestCase):
         '''Testing voices making routines within make notation
         '''
         from music21 import stream
+        from music21.instrument import Xylophone
         s = stream.Stream()
+        s.insert(0, Xylophone())
         s.insert(0, note.Note('C4', quarterLength=8))
         s.repeatInsert(note.Note('b-4', quarterLength=0.5), [x * 0.5 for x in range(16)])
         s.repeatInsert(note.Note('f#5', quarterLength=2), [0, 2, 4, 6])
@@ -3912,6 +3921,9 @@ class Test(unittest.TestCase):
             for n in m.voices[1].notes:  # middle voice has beams
                 self.assertGreater(len(n.beams), 0)
 
+        # check instruments
+        self.assertIsInstance(sPost.getInstruments(recurse=True)[0], Xylophone)
+
     def testMakeNotationC(self):
         '''Test creating diverse, overlapping durations and notes
         '''