Repository: cuthbertLab/music21 (python)

Issue:
developerReference documentation: avoiding sphinx formatting
In the [developerReference documentation for documenting](http://web.mit.edu/music21/doc/developerReference/documenting.html), there's an example for how to avoid sphinx formatting which claims:
- line breaks and spacing is preserved
- bold sphinx formatting is not observed

... but the bold still seems to be observed in the example and as that's the only expected change, it looks likely that it's all simply still formatted.

No PR because I'm out of my depth here but some thoughts based on the instructions:
- Space(/s) before after (possibly missing, though there are line breaks)
- (Related) position of the double colon

Screenshot:
<img width="818" alt="BoldIsObserved" src="https://user-images.githubusercontent.com/19486660/105630471-595bb680-5e41-11eb-96be-7b76702625c9.png">


Pull request description:
Update Sphinx example for block-quoting code
Fixes #790 
New screenshot:

![block-quoting-updated](https://user-images.githubusercontent.com/38668450/105633986-b77c9f80-5e29-11eb-84cf-eb69bfde54a5.png)


Patch:
diff --git a/documentation/source/developerReference/documenting.rst b/documentation/source/developerReference/documenting.rst
index 5487b2659..c124132f4 100644
--- a/documentation/source/developerReference/documenting.rst
+++ b/documentation/source/developerReference/documenting.rst
@@ -137,36 +137,35 @@ The documentation looks like this:
 .. image:: images/completebach.*
     :width: 300
 
-Finally, if there is a section of your documentation that you'd rather sphinx
+Finally, if there is a section of your documentation that you'd rather Sphinx
 not format at all, append two colons to the last line of formatted text,
 followed by a space, followed by the *indented* text block, followed by a
-space. Text written after this space will be formatted. For example, in your
-code write:
+space. Text written after this space will be formatted. This is useful for
+block-quoting example code. For example, in your code write::
 
-::
-
-    ...blah blah blah this text is formatted. now I'm ready for not-formatted
-    text, so I put two colons::
+    ...blah blah blah this text is formatted. Now I want to block-quote
+    some example code, so I put two colons::
 
-        this text is NOT formatted it must be indented
+        this text IS NOT formatted
+        it must be indented
 
-        line breaks and spacing is preserved **bold** sphinx formatting is not
-        observed
+        line breaks AND spacing will be preserved
+        **bold** sphinx formatting unobserved
 
-    Now I'm back to sphinx formatting...**now this is bold!*
+    Now I am back to Sphinx formatting, outside the block. **now this is bold!**
 
 The documentation looks like this:
 
-...blah blah blah this text is formatted. now I'm ready for not-formatted text,
-so I put two colons:
+...blah blah blah this text is formatted. Now I want to block-quote
+some example code, so I put two colons::
 
-    this text is NOT formatted
+    this text IS NOT formatted
     it must be indented
 
-    line breaks and spacing is preserved
-    **bold** sphinx formatting is not observed
+    line breaks AND spacing will be preserved
+    **bold** sphinx formatting unobserved
 
-Now I'm back to sphinx formatting. **now this is bold!**
+Now I am back to Sphinx formatting, outside the block. **now this is bold!**
 
 
 Displaying only some of the test code in the documentation
diff --git a/music21/base.py b/music21/base.py
index fa3397e17..2cf9daa41 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -3210,7 +3210,7 @@ class Music21Object(prebase.ProtoM21Object):
                     mNumber = m.number
         return mNumber
 
-    def _getMeasureOffset(self, includeMeasurePadding=True):
+    def _getMeasureOffset(self, includeMeasurePadding=True) -> Union[float, fractions.Fraction]:
         # noinspection PyShadowingNames
         '''
         Try to obtain the nearest Measure that contains this object,
@@ -3253,15 +3253,11 @@ class Music21Object(prebase.ProtoM21Object):
             if m is not None:
                 # environLocal.printDebug(['using found Measure for offset access'])
                 try:
+                    offsetLocal = m.elementOffset(self)
                     if includeMeasurePadding:
-                        offsetLocal = m.elementOffset(self) + m.paddingLeft
-                    else:
-                        offsetLocal = m.elementOffset(self)
+                        offsetLocal += m.paddingLeft
                 except SitesException:
-                    try:
-                        offsetLocal = self.offset
-                    except AttributeError:
-                        offsetLocal = 0.0
+                    offsetLocal = self.offset
 
             else:  # hope that we get the right one
                 # environLocal.printDebug(
diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py
index 7fec5dcc6..5634c7861 100644
--- a/music21/chord/__init__.py
+++ b/music21/chord/__init__.py
@@ -3610,7 +3610,8 @@ class Chord(note.NotRest):
             raise ChordException(f'the given pitch is not in the Chord: {pitchTarget}')
 
     def setStemDirection(self, stem, pitchTarget):
-        '''Given a stem attribute as a string and a pitch object in this Chord,
+        '''
+        Given a stem attribute as a string and a pitch object in this Chord,
         set the stem attribute of that pitch to the value of that stem. Valid
         stem directions are found note.stemDirectionNames (see below).
 
diff --git a/music21/clef.py b/music21/clef.py
index 08ea2d804..4ac9c967e 100644
--- a/music21/clef.py
+++ b/music21/clef.py
@@ -19,11 +19,12 @@ commonly used clefs. Clef objects are often found
 within :class:`~music21.stream.Measure` objects.
 '''
 import unittest
-from typing import Mapping, Optional
+from typing import Mapping, Optional, Iterable, Union
 
 from music21 import base
 from music21 import exceptions21
 from music21 import environment
+from music21 import pitch  # for typing only
 from music21 import style
 
 _MOD = 'clef'
@@ -196,6 +197,84 @@ class Clef(base.Music21Object):
         else:
             return ''
 
+    def getStemDirectionForPitches(
+        self,
+        pitchList: Union[pitch.Pitch, Iterable[pitch.Pitch]],
+        *,
+        firstLastOnly: bool = True,
+        extremePitchOnly: bool = False,
+    ) -> str:
+        # noinspection PyShadowingNames
+        '''
+        Return a string representing the stem direction for a single
+        :class:`~music21.pitch.Pitch` object or a list/tuple/Stream of pitches.
+
+        >>> P = pitch.Pitch
+        >>> bc = clef.BassClef()
+        >>> bc.getStemDirectionForPitches(P('C3'))
+        'up'
+
+        For two pitches, the most extreme pitch determines the direction:
+
+        >>> pitchList = [P('C3'), P('B3')]
+        >>> bc.getStemDirectionForPitches(pitchList)
+        'down'
+
+        If `firstLastOnly` is True (as by default) then only the first and last pitches are
+        examined, as in a beam group.  Here we have C3, B3, C3, so despite the B in bass
+        clef being much farther from the center line than either of the Cs, it is stem up:
+
+        >>> pitchList.append(P('C3'))
+        >>> bc.getStemDirectionForPitches(pitchList)
+        'up'
+
+        If `firstLastOnly` is False, then each of the pitches has a weight on the process
+
+        >>> bc.getStemDirectionForPitches(pitchList, firstLastOnly=False)
+        'down'
+
+        If extremePitchOnly if True, then whatever pitch is farthest from the center line
+        determines the direction, regardless of order.  (default False).
+
+        >>> bc.getStemDirectionForPitches(pitchList, extremePitchOnly=True)
+        'down'
+        >>> pitchList.insert(1, P('C2'))
+        >>> bc.getStemDirectionForPitches(pitchList, extremePitchOnly=True)
+        'up'
+        '''
+        if isinstance(pitchList, pitch.Pitch):
+            pitchList = [pitchList]
+
+        if not pitchList:
+            raise ValueError('getStemDirectionForPitches cannot operate on an empty list')
+
+        if extremePitchOnly:
+            pitchMin = min(pitchList, key=lambda pp: pp.diatonicNoteNum)
+            pitchMax = max(pitchList, key=lambda pp: pp.diatonicNoteNum)
+            relevantPitches = [pitchMin, pitchMax]
+        elif firstLastOnly and len(pitchList) > 1:
+            relevantPitches = [pitchList[0], pitchList[-1]]
+        else:
+            relevantPitches = pitchList
+
+        differenceSum = 0
+        if hasattr(self, 'lowestLine'):
+            midLine = self.lowestLine + 4
+        else:
+            midLine = 35  # assume TrebleClef-like.
+
+        for p in relevantPitches:
+            distanceFromMidLine = p.diatonicNoteNum - midLine
+            differenceSum += distanceFromMidLine
+
+        if differenceSum >= 0:
+            return 'down'
+        else:
+            return 'up'
+
+
+
+
 # ------------------------------------------------------------------------------
 
 
diff --git a/music21/meter.py b/music21/meter.py
index c09ef3c4d..942aa4f14 100644
--- a/music21/meter.py
+++ b/music21/meter.py
@@ -2406,7 +2406,7 @@ class MeterSequence(MeterTerminal):
     # --------------------------------------------------------------------------
     # given a quarter note position, return the active index
 
-    def offsetToIndex(self, qLenPos, includeCoincidentBoundaries=False):
+    def offsetToIndex(self, qLenPos, includeCoincidentBoundaries=False) -> int:
         '''
         Given an offset in quarterLengths (0.0 through self.duration.quarterLength), return
         the index of the active MeterTerminal or MeterSequence
@@ -2446,8 +2446,9 @@ class MeterSequence(MeterTerminal):
         '''
         if qLenPos >= self.duration.quarterLength or qLenPos < 0:
             raise MeterException(
-                'cannot access from qLenPos %s where total duration is %s' % (
-                    qLenPos, self.duration.quarterLength))
+                f'cannot access from qLenPos {qLenPos} '
+                + f'where total duration is {self.duration.quarterLength}'
+            )
 
         qPos = 0
         match = None
@@ -3651,11 +3652,13 @@ class TimeSignature(base.Music21Object):
 
         Pickup measure support included by taking in an additional measureStartOffset argument.
 
-        >>> threeFour = meter.TimeSignature('3/4')
-        >>> nList = [note.Note(type='eighth') for _ in range(3)]
-        >>> beamList = threeFour.getBeams(nList, measureStartOffset=1.5)
+        >>> twoTwo = meter.TimeSignature('2/2')
+        >>> nList = [note.Note(type='eighth') for _ in range(5)]
+        >>> beamList = twoTwo.getBeams(nList, measureStartOffset=1.5)
         >>> print(beamList)
-        [<music21.beam.Beams <music21.beam.Beam 1/start>>,
+        [None,
+         <music21.beam.Beams <music21.beam.Beam 1/start>>,
+         <music21.beam.Beams <music21.beam.Beam 1/continue>>,
          <music21.beam.Beams <music21.beam.Beam 1/continue>>,
          <music21.beam.Beams <music21.beam.Beam 1/stop>>]
         '''
@@ -3674,7 +3677,7 @@ class TimeSignature(base.Music21Object):
         if len(srcList) <= 1:
             return [None for _ in srcList]
 
-        beamsList = beam.Beams.naiveBeams(srcList)  # hold maximum Beams objects
+        beamsList = beam.Beams.naiveBeams(srcList)  # hold maximum Beams objects, all with type None
         beamsList = beam.Beams.removeSandwichedUnbeamables(beamsList)
 
         def fixBeamsOneElementDepth(i, el, depth):
@@ -3724,7 +3727,7 @@ class TimeSignature(base.Music21Object):
                 return
 
             # determine beamType
-            if isFirst:  # if the first, we always start
+            if isFirst and measureStartOffset == 0:  # if the first w/o pickup, we always start
                 beamType = 'start'
                 # get a partial beam if we cannot continue this
                 if (beamNext is None
@@ -3757,6 +3760,9 @@ class TimeSignature(base.Music21Object):
                     # the greater boundary of this span), and we did not have a
                     # beam or beam number in the previous beam
 
+                    # first note in pickup measures might also get 'partial-left'
+                    # here, but this gets fixed in sanitize partial beams
+
                     # may need to check: beamNext is not None and
                     #   beamNumber in beamNext.getNumbers()
                     # note: it is critical that we check archetypeSpan here
@@ -3992,7 +3998,7 @@ class TimeSignature(base.Music21Object):
         >>> ts2.getMeasureOffsetOrMeterModulusOffset(n4)
         0.0
         '''
-        mOffset = el._getMeasureOffset()
+        mOffset = el._getMeasureOffset()  # TODO(msc): expose this method and remove private
         tsMeasureOffset = self._getMeasureOffset(includeMeasurePadding=False)
         if (mOffset + tsMeasureOffset) < self.barDuration.quarterLength:
             return mOffset
@@ -4062,8 +4068,8 @@ class TimeSignature(base.Music21Object):
 
         If you want a fractional number for the beat, see `getBeatProportion`.
 
-        TODO: late: In v.1.4 -- getBeat will probably do what getBeatProportion does now...
-
+        TODO: In v7 -- getBeat will probably do what getBeatProportion does now...
+        but just with 1 added to it.
 
         >>> a = meter.TimeSignature('3/4', 3)
         >>> a.getBeat(0)
@@ -4487,11 +4493,11 @@ class Test(unittest.TestCase):
 
     def testGetBeams(self):
         from music21 import note
-        a = TimeSignature('6/8')
+        ts = TimeSignature('6/8')
         durList = [16, 16, 16, 16, 8, 16, 16, 16, 16, 8]
 
-        b = [note.Note(quarterLength=4 / d) for d in durList]
-        c = a.getBeams(b)
+        notesList = [note.Note(quarterLength=4 / d) for d in durList]
+        beams = ts.getBeams(notesList)
         match = '''[<music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>>,
         <music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/continue>>,
         <music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/continue>>,
@@ -4503,7 +4509,72 @@ class Test(unittest.TestCase):
         <music21.beam.Beams <music21.beam.Beam 1/continue>/<music21.beam.Beam 2/stop>>,
         <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
 
-        self.assertTrue(common.whitespaceEqual(str(c), match))
+        self.assertTrue(common.whitespaceEqual(str(beams), match))
+
+    def test_getBeams_offset(self):
+        '''
+        Test getting Beams from a Measure that has an anacrusis that makes the
+        first note not beamed.
+        '''
+        from music21 import note
+        from music21 import stream
+        m = stream.Measure()
+        m.repeatAppend(note.Note(type='eighth'), 5)
+        ts = TimeSignature('2/2')
+
+        beams = ts.getBeams(m, measureStartOffset=1.5)
+        self.assertIsNone(beams[0])
+        for b in beams[1:]:
+            self.assertIsNotNone(b)
+        match = '''[None,
+        <music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
+        self.assertTrue(common.whitespaceEqual(str(beams), match))
+
+        m.append(note.Note(type='eighth'))
+        beams = ts.getBeams(m, measureStartOffset=1.0)
+        match = '''[<music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>,
+        <music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
+        self.assertTrue(common.whitespaceEqual(str(beams), match), str(beams))
+
+        m = stream.Measure()
+        m.repeatAppend(note.Note(type='eighth'), 5)
+        ts = TimeSignature('3/2')
+
+        beams = ts.getBeams(m, measureStartOffset=3.5)
+        match = '''[None,
+        <music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
+        self.assertTrue(common.whitespaceEqual(str(beams), match))
+
+
+        m = stream.Measure()
+        m.repeatAppend(note.Note(type='eighth'), 4)
+        ts = TimeSignature('6/8')
+        beams = ts.getBeams(m, measureStartOffset=1.0)
+        match = '''[None,
+        <music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
+        self.assertTrue(common.whitespaceEqual(str(beams), match))
+
+        m.append(note.Note(type='eighth'))
+        beams = ts.getBeams(m, measureStartOffset=0.5)
+        match = '''[<music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>,
+        <music21.beam.Beams <music21.beam.Beam 1/start>>,
+        <music21.beam.Beams <music21.beam.Beam 1/continue>>,
+        <music21.beam.Beams <music21.beam.Beam 1/stop>>]'''
+        self.assertTrue(common.whitespaceEqual(str(beams), match), str(beams))
+
 
     def testOffsetToDepth(self):
         # get a maximally divided 4/4 to the level of 1/8
@@ -4936,6 +5007,6 @@ _DOC_ORDER = [TimeSignature]
 
 if __name__ == '__main__':
     import music21
-    music21.mainTest(Test)  # , runTest='testCompoundSameDenominator')
+    music21.mainTest(Test)  # , runTest='test_getBeams_offset')
 
 
diff --git a/music21/stream/__init__.py b/music21/stream/__init__.py
index d441e07f0..27e01e74b 100644
--- a/music21/stream/__init__.py
+++ b/music21/stream/__init__.py
@@ -6051,16 +6051,19 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             displayTiedAccidentals=displayTiedAccidentals,
         )
 
-    def makeBeams(self, *, inPlace=False):
+    def makeBeams(self, *, inPlace=False, setStemDirections=True):
         '''
         Return a new Stream, or modify the Stream in place, with beams applied to all
         notes.
 
         See :py:func:`~music21.stream.makeNotation.makeBeams`.
+
+        New in v6.7 -- setStemDirections.
         '''
         return makeNotation.makeBeams(
             self,
             inPlace=inPlace,
+            setStemDirections=setStemDirections
         )
 
     def makeAccidentals(
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index c8d4309f1..0d9fcbea4 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -14,6 +14,7 @@
 
 import copy
 import unittest
+from typing import List, Generator, Optional
 
 from music21 import beam
 from music21 import clef
@@ -22,6 +23,7 @@ from music21 import defaults
 from music21 import environment
 from music21 import meter
 from music21 import note
+from music21 import pitch
 
 from music21.common.numberTools import opFrac
 
@@ -31,7 +33,13 @@ environLocal = environment.Environment(__file__)
 # -----------------------------------------------------------------------------
 
 
-def makeBeams(s, *, inPlace=False):
+def makeBeams(
+    s,
+    *,
+    inPlace=False,
+    setStemDirections=True,
+):
+    # noinspection PyShadowingNames
     '''
     Return a new Measure, or Stream of Measures, with beams applied to all
     notes. Measures with Voices will process voices independently.
@@ -174,6 +182,8 @@ def makeBeams(s, *, inPlace=False):
                     n.beams = beam.Beams()
 
     del mColl  # remove Stream no longer needed
+    if setStemDirections:
+        setStemDirectionForBeamGroups(returnObj)
 
     returnObj.streamStatus.beams = True
     if inPlace is not True:
@@ -845,6 +855,7 @@ def makeTies(
     inPlace=False,
     displayTiedAccidentals=False
 ):
+    # noinspection PyShadowingNames
     '''
     Given a stream containing measures, examine each element in the
     Stream. If the elements duration extends beyond the measure's boundary,
@@ -1174,6 +1185,7 @@ def makeTies(
 
 
 def makeTupletBrackets(s, *, inPlace=False):
+    # noinspection PyShadowingNames
     '''
     Given a Stream of mixed durations, designates the first and last tuplet of any group
     of tuplets as the start or end of the tuplet, respectively.
@@ -1400,6 +1412,7 @@ def moveNotesToVoices(source, classFilterList=('GeneralNote',)):
 
 
 def getTiePitchSet(prior):
+    # noinspection PyShadowingNames
     '''
     helper method for makeAccidentals to get the tie pitch set (or None)
     from the prior
@@ -1452,12 +1465,167 @@ def getTiePitchSet(prior):
         return tiePitchSet
 
 
+def iterateBeamGroups(
+    s: 'music21.stream.Stream',
+    skipNoBeams=True,
+    recurse=True
+) -> Generator[List[note.NotRest], None, None]:
+    '''
+    Generator that yields a List of NotRest objects that fall within a beam group.
+
+    If `skipNoBeams` is True, then NotRest objects that have no beams are skipped.
+
+    Recurse is True by default.
+
+    Unclosed beam groups (like start followed by a Rest before a stop), currently
+    will continue to yield until the first stop, but this behavior may change at any time in
+    the future as beaming-over-barlines with multiple voices or beaming across
+    Parts or PartStaffs is supported.
+
+    >>> from music21.stream.makeNotation import iterateBeamGroups
+    >>> sc = converter.parse('tinyNotation: 3/4 c8 d e f g4   a4 b8 a16 g16 f4')
+    >>> sc.makeBeams(inPlace=True)
+    >>> for beamGroup in iterateBeamGroups(sc):
+    ...     print(beamGroup)
+    [<music21.note.Note C>, <music21.note.Note D>]
+    [<music21.note.Note E>, <music21.note.Note F>]
+    [<music21.note.Note B>, <music21.note.Note A>, <music21.note.Note G>]
+
+    >>> for beamGroup in iterateBeamGroups(sc, skipNoBeams=False):
+    ...     print(beamGroup)
+    [<music21.note.Note C>, <music21.note.Note D>]
+    [<music21.note.Note E>, <music21.note.Note F>]
+    [<music21.note.Note G>]
+    [<music21.note.Note A>]
+    [<music21.note.Note B>, <music21.note.Note A>, <music21.note.Note G>]
+    [<music21.note.Note F>]
+
+    If recurse is False, assumes a flat Score:
+
+    >>> for beamGroup in iterateBeamGroups(sc, recurse=False):
+    ...     print(beamGroup)
+
+    >>> for beamGroup in iterateBeamGroups(sc.flat, recurse=False):
+    ...     print(beamGroup)
+    [<music21.note.Note C>, <music21.note.Note D>]
+    [<music21.note.Note E>, <music21.note.Note F>]
+    [<music21.note.Note B>, <music21.note.Note A>, <music21.note.Note G>]
+
+    New in v6.7.
+    '''
+    iterator: 'music21.stream.iterator.StreamIterator' = s.recurse() if recurse else s.iter
+    current_beam_group: List[note.NotRest] = []
+    in_beam_group: bool = False
+    for el in iterator.getElementsByClass('NotRest'):
+        first_el_type: Optional[str] = None
+        if el.beams and el.beams.getByNumber(1):
+            first_el_type = el.beams.getTypeByNumber(1)
+
+        if first_el_type == 'start':
+            in_beam_group = True
+        if in_beam_group:
+            current_beam_group.append(el)
+        if first_el_type == 'stop':
+            yield current_beam_group
+            current_beam_group = []
+            in_beam_group = False
+        elif not skipNoBeams and not in_beam_group:
+            yield [el]
+
+    if current_beam_group:
+        yield current_beam_group
+
+
+def setStemDirectionForBeamGroups(
+    s: 'music21.stream.Stream',
+    *,
+    setNewStems=True,
+    overrideConsistentStemDirections=False,
+) -> None:
+    '''
+    Find all beam groups and set all the `stemDirection` tags for notes/chords
+    in a beam group to point either up or down.  If any other stem direction is
+    encountered ('double', 'noStem', etc.) that note is skipped.
+
+    If all notes have the same (non-unspecified) direction, then they are left alone unless
+    `overrideConsistentStemDirections` is True (default: False).  For instance,
+    :meth:`~music21.clef.Clef.getStemDirectionForPitches` might say "down" but
+    if everything in the beamGroup is either
+
+    if `setANewStems` is True (as by default), then even notes with stemDirection
+    of 'unspecified' get a stemDirection.
+
+    Currently assumes that the clef does not change within a beam group.  This
+    assumption may change in the future.
+
+    Operates in place.  Run `copy.deepcopy(s)` beforehand for a non-inPlace version.
+
+    New in v6.7.
+    '''
+    beamGroup: List[note.NotRest]
+    for beamGroup in iterateBeamGroups(s, skipNoBeams=True, recurse=True):
+        setStemDirectionOneGroup(
+            beamGroup,
+            setNewStems=setNewStems,
+            overrideConsistentStemDirections=overrideConsistentStemDirections
+        )
+
+
+def setStemDirectionOneGroup(
+    group: List[note.NotRest],
+    *,
+    setNewStems=True,
+    overrideConsistentStemDirections=False,
+) -> None:
+    '''
+    Helper function to set stem directions for one beam group (or perhaps a beat, etc.)
+
+    See setStemDirectionForBeamGroups for detailed information.
+
+    New in v6.7.
+    '''
+    if not group:  # pragma: no cover
+        return  # should not happen
+
+    up_down_stem_directions = set(n.stemDirection for n in group
+                                  if n.stemDirection in ('up', 'down'))
+    if len(up_down_stem_directions) < 2:
+        has_consistent_stem_directions = True
+    else:
+        has_consistent_stem_directions = False
+
+    # noinspection PyTypeChecker
+    clef_context: clef.Clef = group[0].getContextByClass(clef.Clef)
+    if not clef_context:
+        return
+
+    pitchList: List[pitch.Pitch] = []
+    for n in group:
+        pitchList.extend(n.pitches)
+    groupStemDirection = clef_context.getStemDirectionForPitches(pitchList)
+
+    for n in group:
+        noteDirection = n.stemDirection
+        if noteDirection == 'unspecified' and not setNewStems:
+            continue
+        elif (noteDirection in ('up', 'down')
+              and not overrideConsistentStemDirections
+              and has_consistent_stem_directions):
+            continue
+        elif noteDirection in ('up', 'down', 'unspecified'):
+            n.stemDirection = groupStemDirection
+
+
+
+
+
 # -----------------------------------------------------------------------------
 
 class Test(unittest.TestCase):
     '''
     Note: all Stream tests are found in test/testStream.py
     '''
+    allaBreveBeamTest = "tinyNotation: 2/2 c8 d e f   trip{a b c' a b c'}  f' e' d' G  a b c' d'"
 
     def testNotesToVoices(self):
         from music21 import stream
@@ -1475,6 +1643,69 @@ class Test(unittest.TestCase):
                          '[<music21.note.Note C>, <music21.note.Note C>, '
                          + '<music21.note.Note C>, <music21.note.Note C>]')
 
+    def testSetStemDirectionOneGroup(self):
+        from music21 import converter
+        p = converter.parse(self.allaBreveBeamTest)
+        p.makeBeams(inPlace=True, setStemDirections=False)
+        a, b, c, d = iterateBeamGroups(p)
+
+        def testDirections(group, expected):
+            self.assertEqual(len(group), len(expected))
+            for j in range(len(group)):
+                self.assertEqual(group[j].stemDirection, expected[j])
+
+        testDirections(a, ['unspecified'] * 4)
+        setStemDirectionOneGroup(a, setNewStems=False)
+        testDirections(a, ['unspecified'] * 4)
+        setStemDirectionOneGroup(a)
+        testDirections(a, ['up'] * 4)
+        for n in a:
+            n.stemDirection = 'down'
+        setStemDirectionOneGroup(a)
+        testDirections(a, ['down'] * 4)
+        setStemDirectionOneGroup(a, overrideConsistentStemDirections=True)
+        testDirections(a, ['up'] * 4)
+
+        setStemDirectionOneGroup(b)
+        testDirections(b, ['down'] * 6)
+
+        # this one is all high but has one very low G
+        setStemDirectionOneGroup(c)
+        testDirections(c, ['up'] * 4)
+
+        dStems = ['down', 'noStem', 'double', 'up']
+        for i, n in enumerate(d):
+            n.stemDirection = dStems[i]
+        setStemDirectionOneGroup(d)
+        testDirections(d, ['down', 'noStem', 'double', 'down'])
+
+    def testSetStemDirectionForBeamGroups(self):
+        from music21 import converter
+        p = converter.parse(self.allaBreveBeamTest)
+        p.makeBeams(inPlace=True, setStemDirections=False)
+        d = list(iterateBeamGroups(p))[-1]
+        dStems = ['down', 'noStem', 'double', 'up']
+        for i, n in enumerate(d):
+            n.stemDirection = dStems[i]
+
+        setStemDirectionForBeamGroups(p)
+        self.assertEqual([n.stemDirection for n in p.flat.notes],
+                         ['up'] * 4 + ['down'] * 6 + ['up'] * 4
+                         + ['down', 'noStem', 'double', 'down']
+                         )
+
+    def testMakeBeamsWithStemDirection(self):
+        from music21 import converter
+        p = converter.parse(self.allaBreveBeamTest)
+        dStems = ['down', 'noStem', 'double', 'up']
+        for i, n in enumerate(p.flat.notes[-4:]):
+            n.stemDirection = dStems[i]
+        p.makeBeams(inPlace=True)
+        self.assertEqual([n.stemDirection for n in p.flat.notes],
+                         ['up'] * 4 + ['down'] * 6 + ['up'] * 4
+                         + ['down', 'noStem', 'double', 'down']
+                         )
+
 
 # -----------------------------------------------------------------------------
 


Test patch:
diff --git a/music21/test/testStream.py b/music21/test/testStream.py
index 2866a3278..bf857ce87 100644
--- a/music21/test/testStream.py
+++ b/music21/test/testStream.py
@@ -8120,8 +8120,6 @@ class Test(unittest.TestCase):
     def test_makeBeams__1_e__after_16th_note(self):
         """
         Test that a 16th+8th notes after a 16th notes have proper beams.
-
-        Note: proper beams repr: https://cl.ly/90ce7b
         """
         m = Measure()
         m.timeSignature = meter.TimeSignature('2/4')
@@ -8144,6 +8142,28 @@ class Test(unittest.TestCase):
         # Now test that they are equal
         self.assertEqual(fourth_note_beams, beams[3])
 
+    def test_makeBeams__paddingLeft_2_2(self):
+        m = Measure()
+        m.timeSignature = meter.TimeSignature('2/2')
+        m.paddingLeft = 1.5  # 5-eighth-note pickup
+        m.repeatAppend(note.Note(type='eighth'), 5)
+
+        m.makeBeams(inPlace=True)
+        beams = self.get_beams_from_stream(m)
+
+        start_beam = beam.Beams()
+        start_beam.append('start')
+        continue_beam = beam.Beams()
+        continue_beam.append('continue')
+        stop_beam = beam.Beams()
+        stop_beam.append('stop')
+
+        self.assertEqual(beams[0], beam.Beams())  # first should have no beams
+        self.assertEqual(beams[1], start_beam)  # second should be start
+        self.assertEqual(beams[2], continue_beam)  # third should be continue
+        self.assertEqual(beams[3], continue_beam)  # fourth should be continue
+        self.assertEqual(beams[4], stop_beam)  # last should be stop
+
     def testOpusWrite(self):
         o = Opus()
         s1 = Score()
@@ -8164,5 +8184,5 @@ class Test(unittest.TestCase):
 
 
 if __name__ == '__main__':
-    music21.mainTest(Test, 'verbose')
+    music21.mainTest(Test, 'verbose')  # , runTest='test_makeBeams__paddingLeft_2_2')