Repository: cuthbertLab/music21
Pull request #946 (2021-04-03 00:14:46)

v7 Stream constructor improvements
Inspired by mailing list 4/2/21

This would be the error if someone tried to follow my advice to `.show('text')` on a stream where iterators were inserted (user error!). Easy improvement.

**Update: ** also add `coreGuard...` check to Stream constructor.

```
>>> src = stream.Stream()
>>> otherStream.append(src.iter)
>>> otherStream.show('t')
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/stream/base.py", line 1844, in elementOffset
    o = self._offsetDict[id(element)][0]
KeyError: 4469622288

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/base.py", line 894, in getOffsetBySite
    raise e
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/base.py", line 880, in getOffsetBySite
    a = site.elementOffset(tryOrigin, returnSpecial=returnSpecial)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/stream/base.py", line 1855, in elementOffset
    raise base.SitesException(
music21.sites.SitesException: an entry for this object 0x10a690610 is not stored in stream <music21.stream.Stream 0x109ffcd90>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/stream/base.py", line 258, in show
    return super().show(fmt=fmt, app=app, **keywords)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/base.py", line 2647, in show
    return formatWriter.show(self,
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/converter/subConverters.py", line 552, in show
    print(obj._reprText(**keywords))
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/stream/base.py", line 2985, in _reprText
    return self.recurseRepr(addEndTimes=addEndTimes,
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/stream/base.py", line 2962, in recurseRepr
    msg.append(singleElement(element, indent))
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/stream/base.py", line 2937, in singleElement
    offGet = in_element.getOffsetBySite(self)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/music21/base.py", line 901, in getOffsetBySite
    raise SitesException(
music21.sites.SitesException: an entry for this object <music21.stream.Score 0x10a2cb7c0> is not stored in stream <music21.stream.Stream 0x109ffcd90>
```

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

Coverage increased (+0.0002%) to 92.153% when pulling **fc57fac7c98fd64c9c057aba70c5b66af2e843a3 on jacobtylerwalls:iterator-guard** into **ebc47a1fbc9d65981bc52928593b4b564ae96cd5 on cuthbertLab:master**.


Patch:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..1cb72f5b1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,27 @@
+---
+name: Feature request
+about: Suggest a feature to be included in music21
+title: ''
+labels: 'enhancement'
+assignees: ''
+
+---
+**Motivation**
+<!-- E.g. does this fill a gap? build on music21's strengths? improve usability? -->
+
+
+**Feature summary**
+
+
+**Proposed implementation**
+
+```
+Optionally, describe the implementation or the interface, in code or pseudocode.
+```
+
+**Intent**
+<!-- Please check one or more boxes -->
+
+- [ ] I plan on implementing this myself.
+- [ ] I am willing to pay to have this feature added.
+- [ ] I am starting a discussion with the hope that community members will volunteer their time to create this. I understand that individuals work on features of interest to them and that this feature may never be implemented.
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..4d778d930
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,114 @@
+`music21` welcomes contributions such as bug reports, new features, fixes, and
+documentation improvements. The
+[project repository](http://www.github.com/cuthbertLab/music21) is hosted at GitHub.
+
+
+## Resources ##
+
+[Module Documentation and User's Guide](http://web.mit.edu/music21/doc/index.html)
+
+[Developer Reference](http://web.mit.edu/music21/doc/developerReference/index.html)
+
+[Mailing List](https://groups.google.com/forum/#!forum/music21list)
+
+[Code of Conduct](README.md)
+
+
+## Issue Tickets ##
+
+Please use the provided templates for bug reports or feature proposals. For issues
+parsing file formats, (i.e. missing/buggy features not translated to or from that
+format), please add to (or start) a general ticket for all support requests for that format.
+(Some formats are no longer being actively maintained, such as MEI, LilyPond, and MuseData,
+but well-tested patches can be merged.)
+
+Good bug reports clearly state the unexpected behavior and include a short set of
+commands to reproduce the issue. If your example requires loading a file, a two-measure
+file is appropriate; a symphony movement is not--that's a support request. GitHub
+is not a support channel.
+
+Speaking of support, helpful people hang out on the
+[mailing list](https://groups.google.com/forum/#!forum/music21list)
+or Stack Overflow.
+
+
+## Submitting Pull Requests ##
+
+Open an issue to propose a feature or report a bug before raising a pull request.
+You can include a diff or link to your own repo if you've already started some of the work.
+(Changes where the motivation is self-evident, like handling exceptions or increasing
+test coverage, don't need issue tickets.) A draft pull request can also sometimes
+help resolve design questions faster.
+
+If your PR fixes an existing issue, please use a GitHub keyword ("Fixes #NNNN")
+in the body of the PR so that the issue will be closed on merge.
+
+You can run locally the same tests that GitHub Actions will run. Install `pylint`
+and `flake8` and run them against the files you edited, and give the test suite a go,
+also. (Quick start: run `python3 -m music21.test.multiprocessTest`.)
+If you miss something and GitHub Actions pesters you with red "X"s, just
+expand the details to get the line-by-line violations, and push another commit.
+
+Speaking of commits, use incremental commits with descriptive messages rather
+than force-pushing, unless the reviewer asked you to or the approach is changing
+completely. Force-pushing prevents the reviewer from assessing what changed between
+reviews. (If this doesn't sound familiar--don't worry!)
+
+
+## Style Guide ##
+
+`music21` began in Perl. Now it interfaces with the TypeScript package `music21j`.
+For these reasons, as well as for backward compatibility, we adhere to `camelCase`
+naming--rather than PEP8 `snake_case` naming--for public APIs such as method names,
+arguments, and the like.
+
+That said, snake_case is acceptable (even encouraged!) in new contributions
+for internal variables that are not exposed publicly. Screen readers can
+pronounce these names better.
+
+Conventions:
+
+  - `'strings MUST be single-quoted, but "double quotes" are allowed internally'`
+  - variable names:
+    - need to be unambiguous
+    - must begin with lowercase
+    - snake_case is encouraged for private variables
+  - line lengths are capped at 100, but if approaching this limit, look for ways to avoid one-lining
+  - line continuation characters (`\`) are not allowed; use open parentheses
+  - prefer f-strings to `%` or `.format()`
+  - annotating types is encouraged: e.g. `self.counter: int = 0` or `def makeNoises(): -> List['noise.Noise']`
+  - prefer enums to string configurations
+  - methods:
+    - no more than three positional arguments (in addition to `self`)
+    - keyword arguments should be keyword-only by using `*`
+      to consume any other positional arguments: `def makeNoise(self, volume, *, color=noise.Pink):`
+    - avoid generic `**kwargs`; make keywords explicit
+  - use [Sphinx formatting](http://web.mit.edu/music21/doc/developerReference/documenting.html#documenting-modules-and-classes)
+      to link to classes and methods in docstrings
+  - use descriptive pull request titles (rather than GitHub's default "Update pitch.py")
+
+
+## Testing ##
+
+We write doctests and unit tests, and we strive for the total
+test coverage of the project to increase with every pull request. See the
+[developer docs](http://web.mit.edu/music21/doc/developerReference/index.html)
+to dig in to specific topics like adjusting doctests to prevent
+actions we don't want executed when running the general test suite (such as opening
+browser windows or playing music).
+
+Pull requests that increase or improve coverage of existing features are very welcome.
+Coverage reports can be found at [Coveralls](https://coveralls.io/github/cuthbertLab/music21).
+
+For changes to file parsing, please test both import and export (when supported for
+that format), and please increment the most minor version number in `music21.__version__`
+so that cached files will be invalidated. Your tests can use `converter.parse(fp, forceSource=True)`
+so that tests have a chance to fail locally, but in most cases we will ask you to 
+remove this keyword when polishing the patch.
+
+
+## Finally ##
+
+If you're looking for ways to get started, browse the issues board, the Coveralls module
+coverage, or the TODO stubs in an area of the code that interests you.
+Thanks for your interest in contributing to music21. We look forward to seeing your work!
diff --git a/README.md b/README.md
index 2a05172b1..1ddda9911 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,10 @@ is available also on GitHub.
 
 See: https://groups.google.com/forum/#!forum/music21list
 
+## Contributing Guide ##
+
+[Contributing Guide](CONTRIBUTING.md)
+
 ## Community Code of Conduct ##
 
 Music21 encourages contributions, discussions, and usage from all people interested in
diff --git a/documentation/source/developerReference/documenting.rst b/documentation/source/developerReference/documenting.rst
index 324de6c0c..2c22a8a1a 100644
--- a/documentation/source/developerReference/documenting.rst
+++ b/documentation/source/developerReference/documenting.rst
@@ -368,7 +368,7 @@ Documenting Class-Level Methods
 This is the most common type of documentation, and it ensures both excellent
 documentation and doctests. A typical example of source code might look like this::
 
-    class className():
+    class ClassName(base.Music21Object):
 
         [instance variables, __init__, etc.]
 
@@ -376,7 +376,7 @@ documentation and doctests. A typical example of source code might look like thi
             '''
             This is documentation for this method
 
-            >>> myInstance = className()
+            >>> myInstance = ClassName()
             >>> myInstance.myNewMethod(someParameters)
             >>> myUnicorn.someInstanceVariable
             'value'
diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py
index c5da5e381..d8df59203 100644
--- a/music21/converter/__init__.py
+++ b/music21/converter/__init__.py
@@ -1358,12 +1358,8 @@ class TestExternal(unittest.TestCase):  # pragma: no cover
         urlBase = 'http://kern.ccarh.org/cgi-bin/ksdata?l=users/craig/classical/'
         urlB = urlBase + 'schubert/piano/d0576&file=d0576-06.krn&f=kern'
         urlC = urlBase + 'bach/cello&file=bwv1007-01.krn&f=xml'
-        for url in [urlB, urlC]:
-            try:
-                unused_post = parseURL(url)
-            except:
-                print(url)
-                raise
+        unused_post = parseURL(urlB)
+        unused_post = parseURL(urlC)
 
     def testFreezer(self):
         from music21 import corpus
@@ -1949,8 +1945,8 @@ class Test(unittest.TestCase):
     def testParseURL(self):
         from music21.humdrum.spineParser import HumdrumException
 
-        urlBase = 'http://kern.ccarh.org/cgi-bin/ksdata?l=users/craig/classical/'
-        url = urlBase + 'chopin/prelude&file=prelude28-20.krn&format=kern'
+        urlBase = 'https://raw.githubusercontent.com/craigsapp/chopin-preludes/'
+        url = urlBase + 'f8fb01f09d717e84929fb8b2950f96dd6bc05686/kern/prelude28-20.krn'
 
         e = environment.Environment()
         e['autoDownload'] = 'allow'
diff --git a/music21/duration.py b/music21/duration.py
index 051d1f7a3..4098abe55 100644
--- a/music21/duration.py
+++ b/music21/duration.py
@@ -653,11 +653,11 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion:
     # Tuplets...
     qLen = opFrac(qLen)
     # try match to type, get next lowest for next part...
-    closestSmallerType, unused_match = quarterLengthToClosestType(qLen)
     try:
+        closestSmallerType, unused_match = quarterLengthToClosestType(qLen)
         nextLargerType(closestSmallerType)
     except DurationException:
-        # too big...
+        # too big or too small
         return QuarterLengthConversion((DurationTuple(type='inexpressible',
                                                       dots=0,
                                                       quarterLength=qLen),), None)
@@ -697,7 +697,13 @@ def quarterConversion(qLen: OffsetQLIn) -> QuarterLengthConversion:
 
     # 8 tied components was not enough.
     # last resort: put one giant tuplet over it.
-    tuplet, component = quarterLengthToNonPowerOf2Tuplet(qLen)
+    try:
+        tuplet, component = quarterLengthToNonPowerOf2Tuplet(qLen)
+    except DurationException:
+        # Failures include 1/72
+        return QuarterLengthConversion((DurationTuple(type='inexpressible',
+                                                      dots=0,
+                                                      quarterLength=qLen),), None)
     return QuarterLengthConversion((component,), tuplet)
 
 
@@ -2119,7 +2125,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
 
         For an 'inexpressible' duration, the opposite of consolidate is
         to set the duration's quarterLength to itself.  It won't necessarily
-        return to the original components, but it will (usually? always?)
+        return to the original components, but it will usually
         create something that can be notated.
 
         >>> a.quarterLength = a.quarterLength
@@ -3649,6 +3655,16 @@ class Test(unittest.TestCase):
             Duration(fractions.Fraction(6 / 7)).fullName
         )
 
+    def testTinyDuration(self):
+        # e.g. delta from chordify: 1/9 - 1/8 = 1/72
+        # exercises quarterLengthToNonPowerOf2Tuplet()
+        d = Duration(1 / 72)
+        self.assertEqual(d.type, 'inexpressible')
+
+        # this failure happens earlier in quarterConversion()
+        d = Duration(1 / 2049)
+        self.assertEqual(d.type, 'inexpressible')
+
 
 # -------------------------------------------------------------------------------
 # define presented order in documentation
diff --git a/music21/meter/tools.py b/music21/meter/tools.py
index d1eafd0a4..2ea79b17e 100644
--- a/music21/meter/tools.py
+++ b/music21/meter/tools.py
@@ -48,7 +48,7 @@ def slashToTuple(value: str) -> Optional[MeterTerminalTuple]:
     '''
     # split by numbers, include slash
     valueNumbers, valueChars = common.getNumFromStr(value,
-                                                    numbers='0123456789/')
+                                                    numbers='0123456789/.')
     valueNumbers = valueNumbers.strip()  # remove whitespace
     valueChars = valueChars.strip()  # remove whitespace
     if 'slow' in valueChars.lower():
@@ -123,6 +123,10 @@ def slashMixedToFraction(valueSrc: str) -> Tuple[NumDenomTuple, bool]:
     Traceback (most recent call last):
     music21.exceptions21.MeterException: cannot match denominator to numerator in: 3+2+5/8+3/4+2+1+4
 
+    >>> meter.tools.slashMixedToFraction('3.0/4.0')
+    Traceback (most recent call last):
+    music21.exceptions21.TimeSignatureException: Cannot create time signature from "3.0/4.0"
+
     Changed in v7 -- new location and returns a tuple as first value.
     '''
     pre = []
@@ -134,7 +138,8 @@ def slashMixedToFraction(valueSrc: str) -> Tuple[NumDenomTuple, bool]:
         if '/' in part:
             tup = slashToTuple(part)
             if tup is None:
-                raise TimeSignatureException('cannot create time signature from:', valueSrc)
+                raise TimeSignatureException(
+                    f'Cannot create time signature from "{valueSrc}"')
             pre.append([tup.numerator, tup.denominator])
         else:  # its just a numerator
             try:
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index ea2a14abf..9fc832cdc 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -64,12 +64,18 @@ def typeToMusicXMLType(value):
     'long'
     >>> musicxml.m21ToXml.typeToMusicXMLType('quarter')
     'quarter'
+    >>> musicxml.m21ToXml.typeToMusicXMLType('inexpressible')
+    Traceback (most recent call last):
+    music21.musicxml.xmlObjects.MusicXMLExportException:
+    Cannot convert inexpressible durations to MusicXML.
     '''
     # MusicXML uses long instead of longa
     if value == 'longa':
         return 'long'
     elif value == '2048th':
         raise MusicXMLExportException('Cannot convert "2048th" duration to MusicXML (too short).')
+    elif value == 'inexpressible':
+        raise MusicXMLExportException('Cannot convert inexpressible durations to MusicXML.')
     else:
         return value
 
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 7601297c7..fda2c5734 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -18,6 +18,8 @@ import re
 # import sys
 # import traceback
 import unittest
+
+from math import isclose
 from typing import List, Optional, Dict, Tuple, Set
 
 import xml.etree.ElementTree as ET
@@ -2399,20 +2401,46 @@ class MeasureParser(XMLParserBase):
 
     def xmlBackup(self, mxObj):
         '''
-        parse a backup tag by changing .offsetMeasureNote
+        Parse a backup tag by changing :attr:`offsetMeasureNote`.
+
+        A floor of 0.0 is enforced in case of float rounding issues.
+
+        >>> MP = musicxml.xmlToM21.MeasureParser()
+        >>> MP.divisions = 100
+        >>> MP.offsetMeasureNote = 1.9979
+
+        >>> from xml.etree.ElementTree import fromstring as EL
+        >>> mxBackup = EL('<backup><duration>100</duration></backup>')
+        >>> MP.xmlBackup(mxBackup)
+        >>> MP.offsetMeasureNote
+        0.9979
+
+        >>> MP.xmlBackup(mxBackup)
+        >>> MP.offsetMeasureNote
+        0.0
         '''
-        try:
-            change = float(mxObj.find('duration').text.strip()) / self.divisions
+        mxDuration = mxObj.find('duration')
+        if mxDuration is not None and textStripValid(mxDuration):
+            change = common.numberTools.opFrac(
+                float(mxDuration.text.strip()) / self.divisions
+            )
             self.offsetMeasureNote -= change
-        except AttributeError:
-            pass
+            if self.offsetMeasureNote < 0:
+                # this could happen if the musicxml durations have errors
+                # https://github.com/cuthbertLab/music21/issues/971
+                self.offsetMeasureNote = 0.0
 
     def xmlForward(self, mxObj):
         '''
-        parse a forward tag by changing .offsetMeasureNote
+        Parse a forward tag by changing :attr:`offsetMeasureNote`.
         '''
-        change = float(mxObj.find('duration').text.strip()) / self.divisions
-        self.offsetMeasureNote += change
+        mxDuration = mxObj.find('duration')
+        if mxDuration is not None and textStripValid(mxDuration):
+            change = common.numberTools.opFrac(
+                float(mxDuration.text.strip()) / self.divisions
+            )
+            # Allow overfilled measures for now -- TODO(someday): warn?
+            self.offsetMeasureNote += change
 
     def xmlPrint(self, mxPrint):
         '''
@@ -2589,6 +2617,8 @@ class MeasureParser(XMLParserBase):
         <music21.pitch.Pitch A3>
         >>> c.pitches[1]
         <music21.pitch.Pitch B3>
+        >>> c.duration
+        <music21.duration.Duration unlinked type:quarter quarterLength:0.75>
 
         >>> a = EL('<note><pitch><step>A</step><octave>3</octave></pitch>'
         ...        + qnDuration
@@ -3208,6 +3238,38 @@ class MeasureParser(XMLParserBase):
         'eighth'
         >>> c.dots
         1
+
+        If the `<duration>` doesn't match the `<type>` and `<dots>`,
+        an unlinked duration is created so that `.quarterLength` agrees with
+        `<duration>` but the notated types can still be represented.
+
+        Create a second dot on `mxNote` and parse again, observing the identical
+        `quarterLength`:
+
+        >>> from xml.etree.ElementTree import SubElement
+        >>> unused = SubElement(mxNote, 'dot')
+        >>> c2 = MP.xmlToDuration(mxNote)
+        >>> c2
+        <music21.duration.Duration unlinked type:eighth quarterLength:0.75>
+        >>> c2.quarterLength
+        0.75
+        >>> c2.type
+        'eighth'
+        >>> c2.dots
+        2
+
+        Grace note durations will be converted later to GraceDurations:
+
+        >>> mxDuration = mxNote.find('duration')
+        >>> mxNote.remove(mxDuration)
+        >>> mxGrace = SubElement(mxNote, 'grace')
+        >>> MP.xmlToDuration(mxNote, inputM21=c2)
+        >>> c2
+        <music21.duration.Duration unlinked type:eighth quarterLength:0.0>
+        >>> gn1 = note.Note(duration=c2)
+        >>> gn2 = MP.xmlGraceToGrace(mxGrace, gn1)
+        >>> gn2.duration
+        <music21.duration.GraceDuration unlinked type:zero quarterLength:0.0>
         '''
         numDots = 0
         tuplets = ()
@@ -3221,7 +3283,7 @@ class MeasureParser(XMLParserBase):
         mxDuration = mxNote.find('duration')
         if mxDuration is not None:
             noteDivisions = float(mxDuration.text.strip())
-            qLen = noteDivisions / divisions
+            qLen = common.numberTools.opFrac(noteDivisions / divisions)
         else:
             qLen = 0.0
 
@@ -3249,36 +3311,36 @@ class MeasureParser(XMLParserBase):
             # TODO: empty-placement
 
         # two ways to create durations, raw (from qLen) and cooked (from type, time-mod, dots)
-        if forceRaw:
-            if d is not None:
-                # environLocal.printDebug(['forced to use raw duration', durRaw])
-                durRaw = duration.Duration()  # raw just uses qLen
-                # the qLen set here may not be computable, but is not immediately
-                # computed until setting components
-                durRaw.quarterLength = qLen
-                try:
-                    d.components = durRaw.components
-                except duration.DurationException:  # TODO: Test
-                    if qLen:
-                        qLenRounded = 2.0 ** round(math.log2(qLen))
-                    else:
-                        qLenRounded = 0.0
-                    environLocal.printDebug(
-                        ['mxToDuration',
-                         f'rounding duration to {qLenRounded} as type is not'
-                         + 'defined and raw quarterLength '
-                         + f'({qLen}) is not a computable duration'])
-                    # environLocal.printDebug(['mxToDuration', 'raw qLen', qLen, durationType,
-                    #                         'mxNote:',
-                    #                         ET.tostring(mxNote, encoding='unicode'),
-                    #                         'last mxDivisions:', divisions])
-                    durRaw.quarterLength = qLenRounded
-            else:
-                d = duration.Duration(quarterLength=qLen)
-        else:  # a cooked version builds up from pieces
+        if d is not None:
+            durRaw = duration.Duration(quarterLength=qLen)  # raw just uses qLen
+            # the qLen set here may not be computable, but is not immediately
+            # computed until setting components
+            try:
+                d.components = durRaw.components
+            except duration.DurationException:  # TODO: Test
+                if qLen:
+                    qLenRounded = 2.0 ** round(math.log2(qLen))
+                else:
+                    qLenRounded = 0.0
+                environLocal.printDebug(
+                    ['mxToDuration',
+                        f'rounding duration to {qLenRounded} as type is not'
+                        + 'defined and raw quarterLength '
+                        + f'({qLen}) is not a computable duration'])
+                durRaw.quarterLength = qLen = qLenRounded
+        else:
+            d = duration.Duration(quarterLength=qLen)
+        # can't do this unless we have a type, so if not forceRaw
+        if not forceRaw:  # a cooked version builds up from pieces
             dt = duration.durationTupleFromTypeDots(durationType, numDots)
+            if (dt.quarterLength == qLen) and not tuplets:
+                # raw == cooked, so we're done
+                # but this comparison gives false positives if tuplets are involved
+                # don't bother with isclose; merely trying to short-circuit
+                return d if inputM21 is None else None
             if d is not None:
                 d.clear()
+                d.tuplets = []
                 d.addDurationTuple(dt)
             else:
                 d = duration.Duration(durationTuple=dt)
@@ -3286,6 +3348,12 @@ class MeasureParser(XMLParserBase):
             for tup in tuplets:
                 d.appendTuplet(tup)
 
+            # Second check against qLen (raw), now with tuplets
+            # if not almost equal, create unlinked Duration and set raw qLen
+            if not isclose(d.quarterLength, qLen, abs_tol=1e-7):
+                d.linked = False
+                d.quarterLength = qLen
+
         if inputM21 is None:
             return d
 
@@ -6514,13 +6582,13 @@ class Test(unittest.TestCase):
         '''
         test that a note with nested tuplets gets converted properly.
         '''
-        mxN = '''
+        mxN = f'''
         <note default-x="347">
         <pitch>
           <step>D</step>
           <octave>5</octave>
         </pitch>
-        <duration>4</duration>
+        <duration>{defaults.divisionsPerQuarter * 0.5 * (2/3) * (2/3)}</duration>
         <voice>1</voice>
         <type>eighth</type>
         <time-modification>
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 1ea8d8a05..18dafee3a 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -115,11 +115,16 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
     The duration, however, can be "unlinked" and explicitly
     set independent of the Stream's contents.
 
-    The first element passed to the Stream is an optional list,
-    tuple, or other Stream of music21 objects which is used to
-    populate the Stream by inserting each object at
+    The first element passed to the Stream is an optional single
+    Music21Object or a list, tuple, or other Stream of Music21Objects
+    which is used to populate the Stream by inserting each object at
     its :attr:`~music21.base.Music21Object.offset`
-    property. Other arguments and keywords are ignored, but are
+    property. One special case is when every such object, such as a newly created
+    one, has no offset. Then, so long as the entire list is not composed of
+    non-Measure Stream subclasses like Parts or Voices, each element is appended,
+    creating a sequence of elements in time, rather than synchrony.
+
+    Other arguments and keywords are ignored, but are
     allowed so that subclassing the Stream is easier.
 
     >>> s1 = stream.Stream()
@@ -154,6 +159,41 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
     {0.0} <music21.stream.Part embeddedPart>
         {0.0} <music21.note.Rest rest>
     {1.0} <music21.note.Note E->
+
+    New in v7 -- providing a single element now works:
+
+    >>> s = stream.Stream(meter.TimeSignature())
+    >>> s.first()
+    <music21.meter.TimeSignature 4/4>
+
+    New in v7 -- providing a list of objects or Measures (but not other Stream
+    subclasses such as Parts) now positions sequentially, i.e. appends:
+
+    >>> s2 = stream.Measure([note.Note(), note.Note(), bar.Barline()])
+    >>> s2.show('text')
+    {0.0} <music21.note.Note C>
+    {1.0} <music21.note.Note C>
+    {2.0} <music21.bar.Barline type=regular>
+
+    A list of measures will let each be appended:
+
+    >>> m1 = stream.Measure(n1, number=1)
+    >>> m2 = stream.Measure(note.Rest(), number=2)
+    >>> s3 = stream.Part([m1, m2])
+    >>> s3.show('text')
+    {0.0} <music21.stream.Measure 1 offset=0.0>
+        {1.0} <music21.note.Note E->
+    {1.5} <music21.stream.Measure 2 offset=1.5>
+        {0.0} <music21.note.Rest rest>
+
+    Here, every element is a Stream that's not a Measure, so we instead insert:
+
+    >>> s4 = stream.Score([stream.PartStaff(n1), stream.PartStaff(note.Rest())])
+    >>> s4.show('text')
+    {0.0} <music21.stream.PartStaff 0x...>
+        {1.0} <music21.note.Note E->
+    {0.0} <music21.stream.PartStaff 0x...>
+        {0.0} <music21.note.Rest rest>
     '''
     # this static attributes offer a performance boost over other
     # forms of checking class
@@ -227,14 +267,31 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         # experimental
         self._mutable = True
 
-        if givenElements and common.isIterable(givenElements):
-            # TODO: perhaps convert a single element into a list?
-            for e in givenElements:
-                try:
-                    self.coreInsert(e.offset, e)
-                except (AttributeError, TypeError):
-                    raise StreamException(f'Unable to insert {e}')
-            self.coreElementsChanged()
+        if givenElements is None:
+            return
+
+        if not common.isIterable(givenElements):
+            givenElements = [givenElements]
+
+        # Append rather than insert if every offset is 0.0
+        # but not if every element is a stream subclass other than a Measure
+        # (i.e. Opus, Score, Part, or Voice)
+        append: bool = False
+        try:
+            append = all(e.offset == 0.0 for e in givenElements)
+        except AttributeError:
+            pass  # appropriate failure will be raised by coreGuardBeforeAddElement()
+        if append and all((e.isStream and not e.isMeasure) for e in givenElements):
+            append = False
+
+        for e in givenElements:
+            self.coreGuardBeforeAddElement(e)
+            if append:
+                self.coreAppend(e)
+            else:
+                self.coreInsert(e.offset, e)
+
+        self.coreElementsChanged()
 
     def _reprInternal(self):
         if self.id is not None:
@@ -1972,12 +2029,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         except (ValueError, TypeError):
             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, '
-                                  + 'create a music21.ElementWrapper for the item')
         element = item
 
-        # checks if element is self; possibly performs additional checks
+        # checks if element is self, among other checks
         self.coreGuardBeforeAddElement(element)
         # main insert procedure here
 
@@ -2349,20 +2403,15 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         else:
             item = itemOrList
 
-        try:
-            unused = item.isStream  # will raise attribute error
-            element = item
-        except AttributeError:  # need to wrap
-            # element = music21.ElementWrapper(item)
-            raise StreamException('to put a non Music21Object in a stream, '
-                                  + 'create a music21.ElementWrapper for the item')
+        element = item
+        # checks if element is self, among other checks
+        self.coreGuardBeforeAddElement(element)
+
         # cannot support elements with Durations in the highest time list
         if element.duration.quarterLength != 0:
             raise StreamException('cannot insert an object with a non-zero '
                                   + 'Duration into the highest time elements list')
 
-        # checks of element is self; possibly performs additional checks
-        self.coreGuardBeforeAddElement(element)
         self.coreStoreAtEnd(element)
         # Streams cannot reside in end elements, thus do not update is flat
         self.coreElementsChanged(updateIsFlat=False)
@@ -10850,7 +10899,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         Changed in v. 5 -- inPlace is default False and a keyword only arg.
 
-        >>> s = stream.Stream([note.Note(), note.Note(), note.Note()])  # simultaneous
+        >>> s = stream.Stream(note.Note())
+        >>> s.insert(0, note.Note())
+        >>> s.insert(0, note.Note())
         >>> s.makeVoices(inPlace=True)
         >>> len(s.voices)
         3
diff --git a/music21/stream/core.py b/music21/stream/core.py
index 2a9ea40b0..62e974091 100644
--- a/music21/stream/core.py
+++ b/music21/stream/core.py
@@ -32,6 +32,7 @@ from music21.common.numberTools import opFrac
 from music21 import spanner
 from music21 import tree
 from music21.exceptions21 import StreamException, ImmutableStreamException
+from music21.stream.iterator import StreamIterator
 
 # pylint: disable=attribute-defined-outside-init
 class StreamCoreMixin:
@@ -381,10 +382,15 @@ class StreamCoreMixin:
     # --------------------------------------------------------------------------
     def coreGuardBeforeAddElement(self, element, *, checkRedundancy=True):
         '''
-        Before adding an element, this method provides
-        important checks to that element.
+        Before adding an element, this method performs
+        important checks on that element.
 
-        Used by both insert() and append()
+        Used by:
+
+          - :meth:`~music21.stream.Stream.insert`
+          - :meth:`~music21.stream.Stream.append`
+          - :meth:`~music21.stream.Stream.storeAtEnd`
+          - `Stream.__init__()`
 
         Returns None or raises a StreamException
 
@@ -392,11 +398,27 @@ class StreamCoreMixin:
         >>> s.coreGuardBeforeAddElement(s)
         Traceback (most recent call last):
         music21.exceptions21.StreamException: this Stream cannot be contained within itself
+
+        >>> s.append(s.iter)
+        Traceback (most recent call last):
+        music21.exceptions21.StreamException: cannot insert StreamIterator into a Stream
+        Iterate over it instead (User's Guide chs. 6 and 26)
+
+        >>> s.insert(4, 3.14159)
+        Traceback (most recent call last):
+        music21.exceptions21.StreamException: to put a non Music21Object in a stream,
+        create a music21.ElementWrapper for the item
         '''
-        # using id() here b/c we do not want to get __eq__ comparisons
         if element is self:  # cannot add this Stream into itself
             raise StreamException('this Stream cannot be contained within itself')
+        if not isinstance(element, Music21Object):
+            if isinstance(element, StreamIterator):
+                raise StreamException('cannot insert StreamIterator into a Stream\n'
+                    "Iterate over it instead (User's Guide chs. 6 and 26)")
+            raise StreamException('to put a non Music21Object in a stream, '
+                                  'create a music21.ElementWrapper for the item')
         if checkRedundancy:
+            # using id() here b/c we do not want to get __eq__ comparisons
             idElement = id(element)
             if idElement in self._offsetDict:
                 # now go slow for safety -- maybe something is amiss in the index.
diff --git a/music21/variant.py b/music21/variant.py
index 8bc9bca7b..c65b7cc48 100644
--- a/music21/variant.py
+++ b/music21/variant.py
@@ -926,7 +926,7 @@ def addVariant(
     {0.0} <music21.note.Note E>
     {1.0} <music21.note.Note E>
     {2.0} <music21.note.Note E>
-    {3.0} <music21.variant.Variant object of length 0.0>
+    {3.0} <music21.variant.Variant object of length 6.0>
     {3.0} <music21.note.Note E>
     {4.0} <music21.note.Note E>
     {5.0} <music21.note.Note E>
@@ -947,12 +947,7 @@ def addVariant(
             tempVariant.append(sVariant)
         else:  # sVariant is not a measure
             sVariantMeasures = sVariant.getElementsByClass('Measure')
-            # apparently expression cannot be simplified. -- this is a mistake
-            # since sVariantMeasures will never == [] even if there are no measures.
-            # yet switching this to `if not sVariantMeasures` breaks things.
-            # TODO(msc) -- figure this out and fix it.
-            # noinspection PySimplifyBooleanCheck
-            if sVariantMeasures == []:  # If there are no measures, work element-wise
+            if not sVariantMeasures:  # If there are no measures, work element-wise
                 for e in sVariant:
                     offset = e.getOffsetBySite(sVariant) + startOffset
                     tempVariant.insert(offset, e)
@@ -2584,7 +2579,9 @@ class Test(unittest.TestCase):
         vn1 = note.Note('F#4')
         vn2 = note.Note('A-4')
 
-        v1 = Variant([vn1, vn2])
+        v1 = Variant()
+        v1.insert(0, vn1)
+        v1.insert(0, vn2)
         v1Copy = copy.deepcopy(v1)
         # copies stored objects; they point to the different Notes vn1/vn2
         self.assertIsNot(v1Copy[0], v1[0])
@@ -2614,7 +2611,9 @@ class Test(unittest.TestCase):
         s.repeatAppend(note.Note('G4'), 8)
         vn1 = note.Note('F#4')
         vn2 = note.Note('A-4')
-        v1 = Variant([vn1, vn2])
+        v1 = Variant()
+        v1.insert(0, vn1)
+        v1.insert(0, vn2)
         s.insert(5, v1)
 
         # as we deepcopy the elements in the variants, we have new Notes


Test patch:
diff --git a/music21/musicxml/testTab.xml b/music21/musicxml/testTab.xml
index df63b5fe4..7a81ec482 100644
--- a/music21/musicxml/testTab.xml
+++ b/music21/musicxml/testTab.xml
@@ -9,6 +9,12 @@
     </part-list>
     <part id="P1">
         <measure number="0">
+            <attributes>
+                <time>
+                    <beats>4</beats>
+                    <beat-type>4</beat-type>
+                </time>
+            </attributes>
             <note>
                 <type>quarter</type>
                 <notations>
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index eb18e713d..f68d26540 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -7927,6 +7927,14 @@ class Test(unittest.TestCase):
             # print(note1.id, note2.id, note3.id, note4.id)
         # TEST???
 
+    def testCoreGuardBeforeAddElement(self):
+        n = note.Note()
+        s = Stream()
+        with self.assertRaises(StreamException):
+            Stream([n, n])
+        with self.assertRaises(StreamException):
+            Stream([n, None, s.iter])
+
     # REMOVED: Turns out that it DOES have fermata on every note!
     # def testSchoenbergChordifyFermatas(self):
     #     '''