Repository: cuthbertLab/music21
Pull request #1589 (2023-06-02 06:38:13)

Set ABC version from I:abc-version information
Previously only `%abc-2.1` on the first line was enough to set the version number.  Now `I:abc-version 2.1` can be set anywhere.

Note that unlike the 2.1 format, the version Information header affects everything from that point forward within one song.  The last version does take effect, but different versions may be given within the same file and the one in effect at that moment will carry forward.

Added lots of typing and fixed some bugs.


Patch:
diff --git a/music21/abcFormat/__init__.py b/music21/abcFormat/__init__.py
index 7b0f55206..3210dfdd0 100644
--- a/music21/abcFormat/__init__.py
+++ b/music21/abcFormat/__init__.py
@@ -69,6 +69,7 @@ import typing as t
 import unittest
 
 from music21 import common
+from music21 import defaults
 from music21 import environment
 from music21 import exceptions21
 from music21 import prebase
@@ -76,6 +77,7 @@ from music21 import prebase
 from music21.abcFormat import translate
 
 if t.TYPE_CHECKING:
+    import pathlib
     from music21 import bar
     from music21 import clef
     from music21 import duration
@@ -123,7 +125,7 @@ reMetadataTag = re.compile('[A-Zw]:')
 rePitchName = re.compile('[a-gA-Gz]')
 reChordSymbol = re.compile('"[^"]*"')  # non-greedy
 reChord = re.compile('[.*?]')  # non-greedy
-reAbcVersion = re.compile(r'^%abc-((\d+)\.(\d+)\.?(\d+)?)')
+reAbcVersion = re.compile(r'%abc-(\d+)\.(\d+)\.?(\d+)?')
 reDirective = re.compile(r'^%%([a-z\-]+)\s+(\S+)(.*)')
 
 
@@ -287,6 +289,29 @@ class ABCMetadata(ABCToken):
             return True
         return False
 
+    def isVersion(self) -> bool:
+        '''
+        Returns True if the tag is "I" for "Information" and
+        the data is "abc-version", False otherwise.
+
+        >>> x = abcFormat.ABCMetadata('I:abc-version 2.1')
+        >>> x.preParse()
+        >>> x.tag
+        'I'
+        >>> x.isVersion()
+        True
+
+        >>> deer = abcFormat.ABCMetadata('I:abc-venison yummy')
+        >>> deer.preParse()
+        >>> deer.tag
+        'I'
+        >>> deer.isVersion()
+        False
+        '''
+        if self.tag == 'I' and self.data.startswith('abc-version '):
+            return True
+        return False
+
     def isMeter(self) -> bool:
         '''
         Returns True if the tag is "M" for meter, False otherwise.
@@ -1614,11 +1639,11 @@ class ABCNote(ABCToken):
             raise ABCTokenException(
                 'cannot calculate quarter length without a default quarter length')
 
-        numStr = []
+        numStrList: list[str] = []
         for c in strSrc:
             if c.isdigit() or c == '/':
-                numStr.append(c)
-        numStr = ''.join(numStr)
+                numStrList.append(c)
+        numStr = ''.join(numStrList)
         numStr = numStr.strip()
 
         # environLocal.printDebug(['numStr', numStr])
@@ -1822,10 +1847,10 @@ class ABCHandler:
     * New in v6.3: lineBreaksDefinePhrases -- does not yet do anything
     '''
     def __init__(self,
-                 abcVersion: tuple[int, ...] | None = None,
+                 abcVersion: tuple[int, ...] = defaults.abcVersionDefault,
                  lineBreaksDefinePhrases=False):
         # tokens are ABC objects import n a linear stream
-        self.abcVersion: tuple[int, ...] | None = abcVersion
+        self.abcVersion: tuple[int, ...] = abcVersion
         self.abcDirectives: dict[str, str] = {}
         self.tokens: list[ABCToken] = []
         self.activeParens: list[str] = []  # e.g. ['Crescendo', 'Slur']
@@ -1833,7 +1858,6 @@ class ABCHandler:
         self.lineBreaksDefinePhrases: bool = lineBreaksDefinePhrases
         self.pos = -1
         self.skipAhead = 0
-        self.isFirstComment = True
         self.strSrc = ''
         self.srcLen: int = len(self.strSrc)  # just documenting this.
         self.currentCollectStr = ''
@@ -1983,57 +2007,57 @@ class ABCHandler:
         'pitch'
         '''
         minVersion = (2, 0, 0)
-        if not self.abcVersion or self.abcVersion < minVersion:
+        if self.abcVersion < minVersion:
             return 'not'
         if 'propagate-accidentals' in self.abcDirectives:
             return self.abcDirectives['propagate-accidentals']
         return 'pitch'  # Default per abc 2.1 standard
 
-    def parseCommentForVersionInformation(self, commentLine: str) -> None:
+    def parseHeaderForVersionInformation(self, inputSearch: str) -> None:
         '''
-        If this is the first comment then searches for a version
-        match and set it as .abcVersion
-
-        If not isFirstComment then does nothing:
+        Search a line of text for a comment abc version number
 
         >>> ah = abcFormat.ABCHandler()
-        >>> ah.abcVersion is None
-        True
-        >>> ah.isFirstComment
-        True
-
-        >>> ah.parseCommentForVersionInformation('%abc-2.3.2')
         >>> ah.abcVersion
-        (2, 3, 2)
-        >>> ah.isFirstComment
-        False
-
-        Now will do nothing since isFirstComment is False
+        (1, 3, 0)
 
-        >>> ah.parseCommentForVersionInformation('%abc-4.9.7')
+        >>> ah.parseHeaderForVersionInformation('%abc-2.3.2')
         >>> ah.abcVersion
         (2, 3, 2)
+
+        Changed in v9: abcVersion defaults to (1, 3, 0) as documented.
         '''
-        if not self.isFirstComment:
-            return None
-        self.isFirstComment = False
-        verMats = reAbcVersion.match(commentLine)
+        verMats = reAbcVersion.search(inputSearch)
         if verMats:
-            abcMajor = int(verMats.group(2))
-            abcMinor = int(verMats.group(3))
-            if verMats.group(4):
-                abcPatch = int(verMats.group(4))
-            else:
-                abcPatch = 0
-            verTuple = (abcMajor, abcMinor, abcPatch)
-            self.abcVersion = verTuple
+            self.abcVersion = self.returnAbcVersionFromMatch(verMats)
 
-    def processComment(self):
+    @staticmethod
+    def returnAbcVersionFromMatch(verMats: re.Match) -> tuple[int, int, int]:
         r'''
-        Processes the comment at self.pos in self.strSrc, setting self.skipAhead,
-        possibly self.abcVersion, and self.abcDirectives for the directiveKey.
+        Given a match from a regular expression return the parsed ABC version
+
+        >>> import re
+        >>> match = re.match('(\d+).(\d+).(\d+)', '2.3.4')
+        >>> ah = abcFormat.ABCHandler()
+        >>> ah.returnAbcVersionFromMatch(match)
+        (2, 3, 4)
 
-        TODO: store the comment in the stream also.
+        >>> match = re.match('(\d+).(\d+).?(\d?)', '1.7')
+        >>> ah.returnAbcVersionFromMatch(match)
+        (1, 7, 0)
+        '''
+        abcMajor = int(verMats.group(1))
+        abcMinor = int(verMats.group(2))
+        if verMats.group(3):
+            abcPatch = int(verMats.group(3))
+        else:
+            abcPatch = 0
+        return (abcMajor, abcMinor, abcPatch)
+
+    def processComment(self):
+        r'''
+        Processes the comment at self.pos in self.strSrc, setting self.skipAhead
+        and self.abcDirectives for the directiveKey.
 
         >>> from textwrap import dedent
         >>> ah = abcFormat.ABCHandler()
@@ -2048,12 +2072,27 @@ class ABCHandler:
         19
         >>> len(' this is a comment\n')
         19
+
+        Directives get stored in the handler:
+
+        >>> data = '%%abc-hello world'
+        >>> ah = abcFormat.ABCHandler()
+        >>> ah.strSrc = data
+        >>> ah.pos = 0
+        >>> ah.processComment()
+        >>> ah.abcDirectives
+        {'abc-hello': 'world'}
+        >>> ah.abcDirectives['abc-hello']
+        'world'
+
+        * Changed in v9: version is not parsed by this method.
         '''
+        # TODO: store the comment in the stream also.
+
         self.skipAhead = self._getNextLineBreak(
             self.strSrc, self.pos
         ) - (self.pos + 1)
         commentLine = self.strSrc[self.pos:self.pos + self.skipAhead + 1]
-        self.parseCommentForVersionInformation(commentLine)
         directiveMatches = reDirective.match(commentLine)
         if directiveMatches:
             directiveKey = directiveMatches.group(1)
@@ -2159,7 +2198,6 @@ class ABCHandler:
         accidentalized: dict[str, str] = {}
         accidental: str = ''
         abcPitch: str = ''  # ABC substring defining any pitch within the current token
-        self.isFirstComment = True
 
         while self.pos < self.srcLen - 1:
             self.pos += 1
@@ -2690,6 +2728,7 @@ class ABCHandler:
             token.parse()
 
     def process(self, strSrc: str) -> None:
+        self.parseHeaderForVersionInformation(strSrc[:100])
         self.tokens = []
         self.tokenize(strSrc)
         self.tokenProcess()
@@ -2773,7 +2812,7 @@ class ABCHandler:
                         return True
         return False
 
-    def splitByReferenceNumber(self):
+    def splitByReferenceNumber(self) -> dict[int | None, ABCHandler]:
         # noinspection PyShadowingNames
         r'''
         Split tokens by reference numbers.
@@ -2782,7 +2821,6 @@ class ABCHandler:
         is used to access the music. If no reference numbers are defined,
         the tune is available under the dictionary entry None.
 
-
         >>> abcStr = 'X:5\nM:6/8\nL:1/8\nK:G\nB3 A3 | G6 | B3 A3 | G6 ||'
         >>> abcStr += 'X:6\nM:6/8\nL:1/8\nK:G\nB3 A3 | G6 | B3 A3 | G6 ||'
         >>> ah = abcFormat.ABCHandler()
@@ -2806,18 +2844,21 @@ class ABCHandler:
 
         Header information (except for comments) should be appended to all pieces.
 
-        >>> abcStrWHeader = '%abc-2.1\nO: Irish\n' + abcStr
+        >>> from textwrap import dedent
+        >>> abcEarly = dedent("""X:4
+        ...    M:6/8
+        ...    L:1/8
+        ...    K:F
+        ...    I:abc-version 1.6
+        ...    B=3 B3 | G6 | B3 A3 | G6 ||
+        ...    """)
+        >>> abcStrWHeader = '%abc-2.1\nO: Irish\n' + abcEarly + abcStr
+
         >>> ah = abcFormat.ABCHandler()
         >>> junk = ah.process(abcStrWHeader)
         >>> len(ah)
-        29
+        44
         >>> ahDict = ah.splitByReferenceNumber()
-        >>> 5 in ahDict
-        True
-        >>> 6 in ahDict
-        True
-        >>> 7 in ahDict
-        False
 
         Did we get the origin header in each score?
 
@@ -2825,11 +2866,28 @@ class ABCHandler:
         <music21.abcFormat.ABCMetadata 'O: Irish'>
         >>> ahDict[6].tokens[0]
         <music21.abcFormat.ABCMetadata 'O: Irish'>
+
+        Before parsing all the tokens should have the 2.1 version:
+
+        >>> ahDict[4].abcVersion
+        (2, 1, 0)
+        >>> ahDict[5].abcVersion
+        (2, 1, 0)
+
+        After parsing, the abcVersion should be set for score 4 and
+        revert for score 5.
+
+        >>> for f in ahDict:
+        ...    _ = abcFormat.translate.abcToStreamScore(ahDict[f])
+        >>> ahDict[4].abcVersion
+        (1, 6, 0)
+        >>> ahDict[5].abcVersion
+        (2, 1, 0)
         '''
         if not self.tokens:
             raise ABCHandlerException('must process tokens before calling split')
 
-        ahDict = {}
+        ahDict: dict[int | None, ABCHandler] = {}
 
         # tokens in this list are prepended to all tunes:
         prependToAllList = []
@@ -2841,7 +2899,7 @@ class ABCHandler:
                 if currentABCHandler is not None:
                     currentABCHandler.tokens = activeTokens
                     activeTokens = []
-                currentABCHandler = ABCHandler()
+                currentABCHandler = ABCHandler(abcVersion=self.abcVersion)
                 referenceNumber = int(token.data)
                 ahDict[referenceNumber] = currentABCHandler
 
@@ -2854,7 +2912,7 @@ class ABCHandler:
             currentABCHandler.tokens = activeTokens
 
         if not ahDict:
-            ahDict[None] = ABCHandler()
+            ahDict[None] = ABCHandler(abcVersion=self.abcVersion)
 
         for thisABCHandler in ahDict.values():
             thisABCHandler.tokens = prependToAllList[:] + thisABCHandler.tokens
@@ -3338,15 +3396,16 @@ class ABCFile(prebase.ProtoM21Object):
     ABC File or String access
 
     The abcVersion attribution optionally specifies the (major, minor, patch)
-    version of ABC to process-- e.g., (1.2.0).
+    version of ABC to process-- e.g., (2, 1, 0).
+
     If not set, default ABC 1.3 parsing is performed.
     '''
-    def __init__(self, abcVersion=None):
-        self.abcVersion = abcVersion
-        self.file = None
-        self.filename = None
+    def __init__(self, abcVersion: tuple[int, int, int] = defaults.abcVersionDefault):
+        self.abcVersion: tuple[int, int, int] = abcVersion
+        self.file: t.IO | None = None
+        self.filename: str | pathlib.Path = ''
 
-    def open(self, filename):
+    def open(self, filename: str | pathlib.Path):
         '''
         Open a file for reading
         '''
@@ -3459,6 +3518,9 @@ class ABCFile(prebase.ProtoM21Object):
             strSrc = self.extractReferenceNumber(strSrc, number)
 
         handler = ABCHandler(abcVersion=self.abcVersion)
+        if self.abcVersion == defaults.abcVersionDefault:
+            handler.parseHeaderForVersionInformation(strSrc[:100])
+
         # return the handler instance
         handler.process(strSrc)
         return handler
diff --git a/music21/abcFormat/translate.py b/music21/abcFormat/translate.py
index db7d1793a..467bb2ef7 100644
--- a/music21/abcFormat/translate.py
+++ b/music21/abcFormat/translate.py
@@ -24,6 +24,7 @@ from __future__ import annotations
 import copy
 import unittest
 import re
+import typing  # cannot import as t because of use of t as token throughout
 
 from music21 import articulations
 from music21 import bar
@@ -40,6 +41,9 @@ from music21 import stream
 from music21 import tempo
 from music21 import tie
 
+if typing.TYPE_CHECKING:
+    from music21 import abcFormat
+
 
 environLocal = environment.Environment('abcFormat.translate')
 
@@ -53,7 +57,11 @@ _abcArticulationsToM21 = {
 }
 
 
-def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None):
+def abcToStreamPart(
+    abcHandler: abcFormat.ABCHandler,
+    inputM21: stream.Part | None = None,
+    spannerBundle: spanner.SpannerBundle | None = None
+) -> stream.Part:
     '''
     Handler conversion of a single Part of a Score with multiple Parts.
     Results are added into the provided inputM21 object
@@ -74,6 +82,9 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None):
 
     clefSet = None
     postTransposition = 0
+    dst: stream.Part | stream.Measure
+
+    mergedHandlers: list[abcFormat.ABCHandler] = []
 
     # need to call on entire handlers, as looks for special criteria,
     # like that at least 2 regular bars are used, not just double bars
@@ -83,7 +94,8 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None):
         barHandlers = abcHandler.splitByMeasure()
         # environLocal.printDebug(['barHandlers', len(barHandlers)])
         # merge loading metadata with each bar that precedes it
-        mergedHandlers = abcFormat.mergeLeadingMetaData(barHandlers)
+        mergedHandlers = typing.cast(list[abcFormat.ABCHandler],
+                                     abcFormat.mergeLeadingMetaData(barHandlers))
         # environLocal.printDebug(['mergedHandlers', len(mergedHandlers)])
     else:  # simply stick in a single list
         mergedHandlers = [abcHandler]
@@ -102,6 +114,8 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None):
     # merged handler are ABCHandlerBar objects, defining attributes for barlines
 
     for mh in mergedHandlers:
+        if typing.TYPE_CHECKING:
+            assert isinstance(mh, abcFormat.ABCHandlerBar)
         # if "use measures" is True and the handler has notes; otherwise add to part
         # environLocal.printDebug(['abcToStreamPart', 'handler', 'left:', mh.leftBarToken,
         #    'right:', mh.rightBarToken, 'len(mh)', len(mh)])
@@ -134,7 +148,7 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None):
                         # only append if created; otherwise, already stored
                         spannerBundle.append(rb)
                     else:  # close it here
-                        rb = rbSpanners[0]  # get RepeatBracket
+                        rb = typing.cast(spanner.RepeatBracket, rbSpanners[0])  # get RepeatBracket
                         rb.addSpannedElements(dst)
                         rb.completeStatus = True
                         # this returns 1 or 2 depending on the repeat
@@ -157,7 +171,7 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None):
                     rbSpanners = spannerBundle.getByClass(
                         spanner.RepeatBracket).getByCompleteStatus(False)
                     if any(rbSpanners):
-                        rb = rbSpanners[0]  # get RepeatBracket
+                        rb = typing.cast(spanner.RepeatBracket, rbSpanners[0])
                         rb.addSpannedElements(dst)
                         rb.completeStatus = True
                         # this returns 1 or 2 depending on the repeat
@@ -201,7 +215,10 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None):
     # following the metadata, or in the open stream
     if not clefSet and not p[clef.Clef]:
         if useMeasures:  # assume at start of measures
-            p.getElementsByClass(stream.Measure).first().clef = clef.bestClef(p, recurse=True)
+            m = p.getElementsByClass(stream.Measure).first()
+            if typing.TYPE_CHECKING:
+                assert m is not None
+            m.clef = clef.bestClef(p, recurse=True)
         else:
             p.insert(0, clef.bestClef(p, recurse=True))
 
@@ -234,7 +251,12 @@ def abcToStreamPart(abcHandler, inputM21=None, spannerBundle=None):
     return p
 
 
-def parseTokens(mh, dst, p, useMeasures):
+def parseTokens(
+    mh: abcFormat.ABCHandler,
+    dst: stream.Measure | stream.Part,
+    p: stream.Part,
+    useMeasures: bool
+) -> tuple[int, bool]:
     '''
     parses all the tokens in a measure or part.
     '''
@@ -245,134 +267,183 @@ def parseTokens(mh, dst, p, useMeasures):
     clefSet = False
     for t in mh.tokens:
         if isinstance(t, abcFormat.ABCMetadata):
-            if t.isMeter():
-                ts = t.getTimeSignatureObject()
-                if ts is not None:  # can be None
-                    # should append at the right position
-                    if useMeasures:  # assume at start of measures
-                        dst.timeSignature = ts
-                    else:
-                        dst.coreAppend(ts)
-            elif t.isKey():
-                ks = t.getKeySignatureObject()
-                if useMeasures:  # assume at start of measures
-                    dst.keySignature = ks
-                else:
-                    dst.coreAppend(ks)
-                # check for clef information sometimes stored in key
-                clefObj, transposition = t.getClefObject()
-                if clefObj is not None:
-                    clefSet = False
-                    # environLocal.printDebug(['found clef in key token:', t,
-                    #     clefObj, transposition])
-                    if useMeasures:  # assume at start of measures
-                        dst.clef = clefObj
-                    else:
-                        dst.coreAppend(clefObj)
-                    postTransposition = transposition
-            elif t.isTempo():
-                mmObj = t.getMetronomeMarkObject()
-                dst.coreAppend(mmObj)
-
+            postTransposition, clefSet = metadataToM21Object(
+                t, dst, postTransposition, clefSet, useMeasures
+            )
         elif isinstance(t, abcFormat.ABCNote):
-            # add the attached chord symbol
-            if t.chordSymbols:
-                cs_name = t.chordSymbols[0]
-                cs_name = re.sub('"', '', cs_name).lstrip().rstrip()
-                cs_name = re.sub('[()]', '', cs_name)
-                cs_name = common.cleanedFlatNotation(cs_name)
-                try:
-                    if cs_name in ('NC', 'N.C.', 'No Chord', 'None'):
-                        cs = harmony.NoChord(cs_name)
-                    elif cs_name.startswith('>'):
-                        continue  # fingering diagram?  Appears in some pieces, ryans-Neumedia
-                    else:
-                        cs = harmony.ChordSymbol(cs_name)
-                    dst.coreAppend(cs, setActiveSite=False)
-                    dst.coreElementsChanged()
-                except ValueError:
-                    pass  # Exclude malformed chord
-
-            # as ABCChord is subclass of ABCNote, handle first
-            if isinstance(t, abcFormat.ABCChord):
-                # Skip an empty chord
-                if not t.subTokens:
-                    continue
-
-                # may have more than notes?
-                pitchNameList = []
-                accStatusList = []  # accidental display status list
-                for tSub in t.subTokens:
-                    # notes are contained as subTokens are already parsed
-                    if isinstance(tSub, abcFormat.ABCNote):
-                        pitchNameList.append(tSub.pitchName)
-                        accStatusList.append(tSub.accidentalDisplayStatus)
-                c = chord.Chord(pitchNameList)
-                c.duration.quarterLength = t.quarterLength
-                if t.activeTuplet:
-                    thisTuplet = copy.deepcopy(t.activeTuplet)
-                    if thisTuplet.durationNormal is None:
-                        thisTuplet.setDurationType(c.duration.type, c.duration.dots)
-                    c.duration.appendTuplet(thisTuplet)
-                # adjust accidental display for each contained pitch
-                for pIndex in range(len(c.pitches)):
-                    if c.pitches[pIndex].accidental is None:
-                        continue
-                    c.pitches[pIndex].accidental.displayStatus = accStatusList[pIndex]
-                dst.coreAppend(c)
-
-                # ql += t.quarterLength
-            else:
-                if t.isRest:
-                    n = note.Rest()
-                else:
-                    n = note.Note(t.pitchName)
-                    if n.pitch.accidental is not None:
-                        n.pitch.accidental.displayStatus = t.accidentalDisplayStatus
-
-                n.duration.quarterLength = t.quarterLength
-                if t.activeTuplet:
-                    thisTuplet = copy.deepcopy(t.activeTuplet)
-                    if thisTuplet.durationNormal is None:
-                        thisTuplet.setDurationType(n.duration.type, n.duration.dots)
-                    n.duration.appendTuplet(thisTuplet)
-
-                # start or end a tie at note n
-                if t.tie is not None:
-                    if t.tie in ('start', 'continue'):
-                        n.tie = tie.Tie(t.tie)
-                        n.tie.style = 'normal'
-                    elif t.tie == 'stop':
-                        n.tie = tie.Tie(t.tie)
-                # Was: Extremely Slow for large Opus files... why?
-                # Answer: some pieces didn't close all their spanners, so
-                # everything was in a Slur/Diminuendo, etc.
-                for span in t.applicableSpanners:
-                    span.addSpannedElements(n)
-
-                if t.inGrace:
-                    n = n.getGrace()
-
-                n.articulations = []
-                while any(t.articulations):
-                    tokenArticulationStr = t.articulations.pop()
-                    if tokenArticulationStr not in _abcArticulationsToM21:
-                        continue
-                    m21ArticulationClass = _abcArticulationsToM21[tokenArticulationStr]
-                    m21ArticulationObj = m21ArticulationClass()
-                    n.articulations.append(m21ArticulationObj)
-
-                dst.coreAppend(n, setActiveSite=False)
-
+            parseABCNote(t, dst)
         elif isinstance(t, abcFormat.ABCSlurStart):
+            if typing.TYPE_CHECKING:
+                assert t.slurObj is not None
             p.coreAppend(t.slurObj)
         elif isinstance(t, abcFormat.ABCCrescStart):
+            if typing.TYPE_CHECKING:
+                assert t.crescObj is not None
             p.coreAppend(t.crescObj)
         elif isinstance(t, abcFormat.ABCDimStart):
+            if typing.TYPE_CHECKING:
+                assert t.dimObj is not None
             p.coreAppend(t.dimObj)
     dst.coreElementsChanged()
     return postTransposition, clefSet
 
+def parseABCNote(
+    t: abcFormat.ABCNote,
+    dst: stream.Measure | stream.Part,
+) -> None:
+    '''
+    Parse an ABCNote object and add it to the destination stream.
+    '''
+    from music21 import abcFormat
+
+    n: note.GeneralNote
+    cs: harmony.ChordSymbol | harmony.NoChord
+
+    # add the attached chord symbol
+    if t.chordSymbols:
+        cs_name = t.chordSymbols[0]
+        cs_name = re.sub('"', '', cs_name).lstrip().rstrip()
+        cs_name = re.sub('[()]', '', cs_name)
+        cs_name = common.cleanedFlatNotation(cs_name)
+        try:
+            if cs_name in ('NC', 'N.C.', 'No Chord', 'None'):
+                cs = harmony.NoChord(cs_name)
+            elif cs_name.startswith('>'):
+                return  # fingering diagram?  Appears in some pieces, ryans-Neumedia
+            else:
+                cs = harmony.ChordSymbol(cs_name)
+            dst.coreAppend(cs, setActiveSite=False)
+            dst.coreElementsChanged()
+        except ValueError:
+            pass  # Exclude malformed chord
+
+    # as ABCChord is subclass of ABCNote, handle first
+    if isinstance(t, abcFormat.ABCChord):
+        # Skip an empty chord
+        if not t.subTokens:
+            return
+
+        # may have more than notes?
+        pitchNameList: list[str] = []
+        accStatusList = []  # accidental display status list
+        for tSub in t.subTokens:
+            # notes are contained as subTokens are already parsed
+            if isinstance(tSub, abcFormat.ABCNote):
+                pn = tSub.pitchName
+                if typing.TYPE_CHECKING:
+                    assert pn is not None
+                pitchNameList.append(pn)
+                accStatusList.append(tSub.accidentalDisplayStatus)
+        c = chord.Chord(pitchNameList)
+        c.duration.quarterLength = t.quarterLength
+        if t.activeTuplet:
+            thisTuplet = copy.deepcopy(t.activeTuplet)
+            if thisTuplet.durationNormal is None:
+                thisTuplet.setDurationType(c.duration.type, c.duration.dots)
+            c.duration.appendTuplet(thisTuplet)
+        # adjust accidental display for each contained pitch
+        for pIndex in range(len(c.pitches)):
+            acc = c.pitches[pIndex].accidental
+            if acc is not None:
+                acc.displayStatus = accStatusList[pIndex]
+        dst.coreAppend(c)
+
+        # ql += t.quarterLength
+    else:
+        if t.isRest:
+            n = note.Rest()
+        else:
+            n = note.Note(t.pitchName)
+            if n.pitch.accidental is not None:
+                n.pitch.accidental.displayStatus = t.accidentalDisplayStatus
+
+        n.duration.quarterLength = t.quarterLength
+        if t.activeTuplet:
+            thisTuplet = copy.deepcopy(t.activeTuplet)
+            if thisTuplet.durationNormal is None:
+                thisTuplet.setDurationType(n.duration.type, n.duration.dots)
+            n.duration.appendTuplet(thisTuplet)
+
+        # start or end a tie at note n
+        if t.tie is not None:
+            if t.tie in ('start', 'continue'):
+                n.tie = tie.Tie(t.tie)
+                n.tie.style = 'normal'
+            elif t.tie == 'stop':
+                n.tie = tie.Tie(t.tie)
+        # Was: Extremely Slow for large Opus files... why?
+        # Answer: some pieces didn't close all their spanners, so
+        # everything was in a Slur/Diminuendo, etc.
+        for span in t.applicableSpanners:
+            span.addSpannedElements(n)
+
+        if t.inGrace:
+            n = n.getGrace()
+
+        n.articulations = []
+        while any(t.articulations):
+            tokenArticulationStr = t.articulations.pop()
+            if tokenArticulationStr not in _abcArticulationsToM21:
+                continue
+            m21ArticulationClass = _abcArticulationsToM21[tokenArticulationStr]
+            m21ArticulationObj = m21ArticulationClass()
+            n.articulations.append(m21ArticulationObj)
+
+        dst.coreAppend(n, setActiveSite=False)
+
+
+def metadataToM21Object(
+    t: abcFormat.ABCMetadata,
+    dst: stream.Measure | stream.Part,
+    postTransposition: int,
+    clefSet: bool,
+    useMeasures: bool,
+) -> tuple[int, bool]:
+    '''
+    Parse a single ABCMetadata token, that does not represent what
+    music21 considers metadata, such as a time signature, key signature,
+    clef, or tempo, and put it in dst.
+
+    This function uses coreAppend operations, so if called separately,
+    dst.coreElementsChanged() should be called before accessing the stream.
+
+    Returns the changed transposition and whether clef was set.
+    '''
+    if t.isMeter():
+        ts = t.getTimeSignatureObject()
+        if ts is not None:  # can be None
+            # should append at the right position
+            if useMeasures:  # assume at start of measures
+                dst.timeSignature = ts
+            else:
+                dst.coreAppend(ts)
+    elif t.isKey():
+        ks = t.getKeySignatureObject()
+        if ks is not None and useMeasures:
+            # assume at start of measures
+            dst.keySignature = ks
+        elif ks is not None:
+            dst.coreAppend(ks)
+        # check for clef information sometimes stored in key
+        clefObj, transposition = t.getClefObject()
+        if clefObj is not None and transposition is not None:
+            # "and transposition is not None" for type checking.
+            clefSet = False
+            # environLocal.printDebug(['found clef in key token:', t,
+            #     clefObj, transposition])
+            if useMeasures:  # assume at start of measures
+                dst.clef = clefObj
+            else:
+                dst.coreAppend(clefObj)
+            postTransposition = transposition
+    elif t.isTempo():
+        mmObj = t.getMetronomeMarkObject()
+        if typing.TYPE_CHECKING:
+            assert mmObj is not None
+        dst.coreAppend(mmObj)
+
+    return postTransposition, clefSet
+
 
 def abcToStreamScore(abcHandler, inputM21=None):
     '''
@@ -401,7 +472,16 @@ def abcToStreamScore(abcHandler, inputM21=None):
     titleCount = 0
     for t in abcHandler.tokens:
         if isinstance(t, abcFormat.ABCMetadata):
-            if t.isTitle():
+            if t.isVersion():
+                v = t.data.replace('abc-version', '').strip()
+                try:
+                    abcHandler.abcVersion = abcHandler.returnAbcVersionFromMatch(
+                        re.match(r'(\d+).(\d+).?(\d+)?', v)
+                    )
+                except AttributeError:
+                    pass
+
+            elif t.isTitle():
                 if titleCount == 0:  # first
                     md.add('title', t.data)
                     # environLocal.printDebug(['got metadata title', t.data])
diff --git a/music21/defaults.py b/music21/defaults.py
index 3bc9c857b..fa6c6b264 100644
--- a/music21/defaults.py
+++ b/music21/defaults.py
@@ -108,6 +108,10 @@ multiMeasureRestMaxSymbols = 11
 # and rewritten on thaw, etc.
 minIdNumberToConsiderMemoryLocation = 100_000_001
 
+
+# abc files without any version information parse as this version
+abcVersionDefault = (1, 3, 0)
+
 # ----------------------------------------------------------------||||||||||||--
 
 
diff --git a/music21/meter/base.py b/music21/meter/base.py
index 861d2720b..1c8614bc6 100644
--- a/music21/meter/base.py
+++ b/music21/meter/base.py
@@ -40,6 +40,7 @@ from music21.meter.core import MeterSequence
 environLocal = environment.Environment('meter')
 
 if t.TYPE_CHECKING:
+    from music21.common.types import OffsetQL
     from music21 import stream
 
 # this is just a placeholder so that .beamSequence, etc. do not need to
@@ -1595,7 +1596,7 @@ class TimeSignature(TimeSignatureBase):
             # create a new object; it will not be linked
             self.displaySequence = MeterSequence(value, partitionRequest)
 
-    def getAccent(self, qLenPos: float) -> bool:
+    def getAccent(self, qLenPos: OffsetQL) -> bool:
         '''
         Return True or False if the qLenPos is at the start of an accent
         division.
diff --git a/music21/spanner.py b/music21/spanner.py
index 34c1b066f..3d5725e43 100644
--- a/music21/spanner.py
+++ b/music21/spanner.py
@@ -242,7 +242,7 @@ class Spanner(base.Music21Object):
         self.idLocal: str | None = None
         # after all spannedElements have been gathered, setting complete
         # will mark that all parts have been gathered.
-        self.completeStatus = False
+        self.completeStatus: bool = False
 
         # data for fill:
 


Test patch:
diff --git a/music21/abcFormat/testFiles.py b/music21/abcFormat/testFiles.py
index 52134fc5d..8fe608b32 100644
--- a/music21/abcFormat/testFiles.py
+++ b/music21/abcFormat/testFiles.py
@@ -757,7 +757,9 @@ class Test(unittest.TestCase):
         from music21.abcFormat import translate
 
         af = abcFormat.ABCFile()
+        self.assertEqual(af.abcVersion, (1, 3, 0))
         ah = af.readstr(directiveCarryOctave)
+        self.assertEqual(ah.abcVersion, (2, 1, 0))
         s = translate.abcToStreamScore(ah)
         notes = s.flatten().getElementsByClass(note.Note)
         gSharp = notes[1]