Repository: cuthbertLab/music21
Pull request #975 (2021-04-21 16:34:39)

Prevent intent boxes from turning into task list
FYI @mscuthbert I don't like how the intent boxes on the feature request template show up as task lists when viewing the rows of the issue board. We don't have to use actual Markdown for this. What do you think?

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

Coverage remained the same at 92.22% when pulling **1c0ccce39c9aaf4a12187633f9906d8c95270989 on jacobtylerwalls:task-list** into **0392c43e7c12c7d8d7edcf2b15abf4ee59603ec9 on cuthbertLab:master**.


Patch:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 1cb72f5b1..71a53f166 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -22,6 +22,6 @@ 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
+[ ] 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/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/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/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):
     #     '''