text: # -*- coding: utf-8 -*-
  # ------------------------------------------------------------------------------
  # Name:         converter/__init__.py
  # Purpose:      Provide a common way to create Streams from any data music21
  #               handles
  #
  # Authors:      Michael Scott Cuthbert
  #               Christopher Ariza
  #
  # Copyright:    Copyright © 2009-2015 Michael Scott Cuthbert and the music21 Project
  # License:      BSD, see license.txt
  # ------------------------------------------------------------------------------
  '''
  music21.converter contains tools for loading music from various file formats,
  whether from disk, from the web, or from text, into
  music21.stream.:class:`~music21.stream.Score` objects (or
  other similar stream objects).
  
  
  The most powerful and easy to use tool is the :func:`~music21.converter.parse`
  function. Simply provide a filename, URL, or text string and, if the format
  is supported, a :class:`~music21.stream.Score` will be returned.
  
  
  This is the most general public interface for all formats.  Programmers
  adding their own formats to the system should provide an interface here to
  their own parsers (such as humdrum, musicxml, etc.)
  
  
  The second and subsequent times that a file is loaded it will likely be much
  faster since we store a parsed version of each file as a "pickle" object in
  the temp folder on the disk.
  
  >>> #_DOCS_SHOW s = converter.parse('d:/myDocs/schubert.krn')
  >>> s = converter.parse(humdrum.testFiles.schubert) #_DOCS_HIDE
  >>> s
  <music21.stream.Score ...>
  '''
  import copy
  import io
  import os
  import re
  import pathlib
  import sys
  import types
  import unittest
  import urllib
  import zipfile
  
  from typing import Union, Tuple
  
  __all__ = [
      'subConverters', 'ArchiveManagerException', 'PickleFilterException',
      'ConverterException', 'ConverterFileException',
      'ArchiveManager', 'PickleFilter', 'resetSubconverters',
      'registerSubconverter', 'unregisterSubconverter',
      'Converter', 'parseFile', 'parseData', 'parseURL',
      'parse', 'freeze', 'thaw', 'freezeStr', 'thawStr',
  
  ]
  
  from music21.converter import subConverters
  
  from music21 import exceptions21
  from music21 import common
  from music21 import stream
  from music21 import musedata as musedataModule
  from music21.metadata import bundles
  from music21 import _version
  from music21 import environment
  
  _MOD = 'converter'
  environLocal = environment.Environment(_MOD)
  
  
  # ------------------------------------------------------------------------------
  class ArchiveManagerException(exceptions21.Music21Exception):
      pass
  
  
  class PickleFilterException(exceptions21.Music21Exception):
      pass
  
  
  class ConverterException(exceptions21.Music21Exception):
      pass
  
  
  class ConverterFileException(exceptions21.Music21Exception):
      pass
  
  
  # ------------------------------------------------------------------------------
  class ArchiveManager:
      r'''
      Before opening a file path, this class can check if this is an
      archived file collection, such as a .zip or or .mxl file. This will return the
      data from the archive.
  
      >>> fnCorpus = corpus.getWork('bwv66.6', fileExtensions=('.xml',))
  
      This is likely a unicode string
  
      >>> #_DOCS_SHOW fnCorpus
      >>> '/Users/cuthbert/git/music21base/music21/corpus/bach/bwv66.6.mxl' #_DOCS_HIDE
      '/Users/cuthbert/git/music21base/music21/corpus/bach/bwv66.6.mxl'
      >>> am = converter.ArchiveManager(fnCorpus)
      >>> am.isArchive()
      True
      >>> am.getNames()
      ['bwv66.6.xml', 'META-INF/container.xml']
      >>> data = am.getData()
      >>> data[0:70]
      '<?xml version="1.0" encoding="UTF-8"?>\r<!DOCTYPE score-partwise PUBLIC'
  
  
      The only archive type supported now is zip. But .mxl is zip...
      '''
      # for info on mxl files, see
      # http://www.recordare.com/xml/compressed-mxl.html
  
      def __init__(self, fp, archiveType='zip'):
          self.fp = common.cleanpath(fp, returnPathlib=True)
          self.archiveType = archiveType
  
      def isArchive(self):
          '''
          Return True or False if the filepath is an
          archive of the supplied archiveType.
          '''
          if self.archiveType == 'zip':
              # some .md files can be zipped
              if self.fp.suffix in ('.mxl', '.md'):
                  # try to open it, as some mxl files are not zips
                  try:
                      unused = zipfile.ZipFile(self.fp, 'r')
                  except zipfile.BadZipfile:
                      return False
                  return True
              elif self.fp.suffix == '.zip':
                  return True
          else:
              raise ArchiveManagerException(f'no support for archiveType: {self.archiveType}')
          return False
  
      def getNames(self):
          '''
          Return a list of all names contained in this archive.
          '''
          post = []
          if self.archiveType == 'zip':
              f = zipfile.ZipFile(self.fp, 'r')
              for subFp in f.namelist():
                  post.append(subFp)
              f.close()
          return post
  
      def getData(self, name=None, dataFormat='musicxml'):
          '''
          Return data from the archive by name. If no name is given,
          a default may be available.
  
          For 'musedata' format this will be a list of strings.
          For 'musicxml' this will be a single string.
          '''
          post = None
          if self.archiveType != 'zip':
              raise ArchiveManagerException(f'no support for extension: {self.archiveType}')
  
          f = zipfile.ZipFile(self.fp, 'r')
  
          if name is None and dataFormat == 'musicxml':  # try to auto-harvest
              # will return data as a string
              # note that we need to read the META-INF/container.xml file
              # and get the root file full-path
              # a common presentation will be like this:
              # ['musicXML.xml', 'META-INF/', 'META-INF/container.xml']
              for subFp in f.namelist():
                  # the name musicXML.xml is often used, or get top level
                  # xml file
                  if 'META-INF' in subFp:
                      continue
                  if not subFp.endswith('.xml') and not subFp.endswith('musicxml'):
                      continue
  
                  post = f.read(subFp)
                  if isinstance(post, bytes):
                      foundEncoding = re.match(br"encoding=[\'\"](\S*?)[\'\"]", post[:1000])
                      if foundEncoding:
                          defaultEncoding = foundEncoding.group(1).decode('ascii')
                          # print('FOUND ENCODING: ', defaultEncoding)
                      else:
                          defaultEncoding = 'UTF-8'
                      try:
                          post = post.decode(encoding=defaultEncoding)
                      except UnicodeDecodeError:  # sometimes windows written...
                          post = post.decode(encoding='utf-16-le')
                          post = re.sub(r"encoding=([\'\"]\S*?[\'\"])",
                                        "encoding='UTF-8'", post)
  
                  break
  
          elif name is None and dataFormat == 'musedata':
              # this might concatenate all parts into a single string
              # or, return a list of strings
              # alternative, a different method might return one at a time
              mdd = musedataModule.MuseDataDirectory(f.namelist())
              # environLocal.printDebug(['mdd object, namelist', mdd, f.namelist])
  
              post = []
              for subFp in mdd.getPaths():
                  # this was f.open(subFp, 'rU') for universal newline support
                  # but that was removed in Python 3.6 and while it is supposed
                  # to be fixed with io.TextIOWrapper, there are no docs that
                  # show how to do so.  -- hopefully fixed
                  try:
                      with f.open(subFp, 'r') as zipOpen:
                          lines = list(io.TextIOWrapper(zipOpen, newline=None))
                  except UnicodeDecodeError:
                      # python3 UTF-8 failed to read haydn/opus103/movement1.zip
                      with f.open(subFp, 'r') as zipOpen:
                          lines = list(io.TextIOWrapper(zipOpen, newline=None,
                                                        encoding='ISO-8859-1'))
                  # environLocal.printDebug(['subFp', subFp, len(lines)])
                  post.append(''.join(lines))
  
                  # note: the following methods do not properly employ
                  # universal new lines; this is a python problem:
                  # http://bugs.python.org/issue6759
                  # post.append(component.read())
                  # post.append(f.read(subFp, 'U'))
                  # msg.append('\n/END\n')
  
          f.close()
  
          return post
  
  
  # ------------------------------------------------------------------------------
  class PickleFilter:
      '''
      Before opening a file path, this class checks to see if there is an up-to-date
      version of the file pickled and stored in the scratch directory.
  
      If forceSource is True, then a pickle path will not be created.
  
      Provide a file path to check if there is pickled version.
  
      If forceSource is True, pickled files, if available, will not be
      returned.
      '''
  
      def __init__(self, fp, forceSource=False, number=None, **keywords):
          self.fp = common.cleanpath(fp, returnPathlib=True)
          self.forceSource = forceSource
          self.number = number
          self.keywords = keywords
          # environLocal.printDebug(['creating pickle filter'])
  
      def getPickleFp(self, directory=None, zipType=None) -> pathlib.Path:
          '''
          Returns the file path of the pickle file for this file.
  
          Returns a pathlib.Path
          '''
          if directory is None:
              directory = environLocal.getRootTempDir()  # pathlibPath
          elif isinstance(directory, str):
              directory = pathlib.Path(directory)
  
          if zipType is None:
              extension = '.p'
          else:
              extension = '.p.gz'
  
          pythonVersion = 'py' + str(sys.version_info.major) + '.' + str(sys.version_info.minor)
  
          pathNameToParse = str(self.fp)
  
          quantization = []
          if 'quantizePost' in self.keywords and self.keywords['quantizePost'] is False:
              quantization.append('noQtz')
          elif 'quarterLengthDivisors' in self.keywords:
              for divisor in self.keywords['quarterLengthDivisors']:
                  quantization.append('qld' + str(divisor))
  
          baseName = '-'.join(['m21', _version.__version__, pythonVersion, *quantization,
                               common.getMd5(pathNameToParse)])
  
          if self.number is not None:
              baseName += '-' + str(self.number)
          baseName += extension
  
          return directory / baseName
  
      def removePickle(self):
          '''
          If a compressed pickled file exists, remove it from disk.
  
          Generally not necessary to call, since we can just overwrite obsolete pickles,
          but useful elsewhere.
          '''
          pickleFp = self.getPickleFp(zipType='gz')  # pathlib...
          if pickleFp.exists():
              os.remove(pickleFp)
  
      def status(self) -> Tuple[pathlib.Path, bool, pathlib.Path]:
          '''
          Given a file path specified with __init__, look for an up to date pickled
          version of this file path. If it exists, return its fp, otherwise return the
          original file path.
  
          Return arguments are file path to load, boolean whether to write a pickle, and
          the file path of the pickle.  All file paths can be pathlib.Path objects or None
  
          Does not check that fp exists or create the pickle file.
  
          >>> fp = '/Users/Cuthbert/Desktop/musicFile.mxl'
          >>> pickFilter = converter.PickleFilter(fp)
          >>> #_DOCS_SHOW pickFilter.status()
          (PosixPath('/Users/Cuthbert/Desktop/musicFile.mxl'), True,
                PosixPath('/tmp/music21/m21-5.0.0-py3.6-18b8c5a5f07826bd67ea0f20462f0b8d.p.gz'))
          '''
          fpScratch = environLocal.getRootTempDir()
          m21Format = common.findFormatFile(self.fp)
  
          if m21Format == 'pickle':  # do not pickle a pickle
              if self.forceSource:
                  raise PickleFilterException(
                      'cannot access source file when only given a file path to a pickled file.')
              writePickle = False  # cannot write pickle if no scratch dir
              fpLoad = self.fp
              fpPickle = None
          elif fpScratch is None or self.forceSource:
              writePickle = False  # cannot write pickle if no scratch dir
              fpLoad = self.fp
              fpPickle = None
          else:  # see which is more up to date
              fpPickle = self.getPickleFp(fpScratch, zipType='gz')  # pathlib Path
              if not fpPickle.exists():
                  writePickle = True  # if pickled file does not exist
                  fpLoad = self.fp
              else:
                  if self.fp.stat().st_mtime < fpPickle.stat().st_mtime:
                      # pickle is most recent
                      writePickle = False
                      fpLoad = fpPickle
                  else:  # file is most recent
                      writePickle = True
                      fpLoad = self.fp
          return fpLoad, writePickle, fpPickle
  
  
  # ------------------------------------------------------------------------------
  _registeredSubconverters = []
  _deregisteredSubconverters = []  # default subconverters to skip
  
  
  def resetSubconverters():
      '''
      Reset state to default (removing all registered and deregistered subconverters).
      '''
      global _registeredSubconverters  # pylint: disable=global-statement
      global _deregisteredSubconverters  # pylint: disable=global-statement
      _registeredSubconverters = []
      _deregisteredSubconverters = []
  
  
  def registerSubconverter(newSubConverter):
      '''
      Add a Subconverter to the list of registered subconverters.
  
      Example, register a converter for the obsolete Amiga composition software Sonix (so fun...)
  
      >>> class ConverterSonix(converter.subConverters.SubConverter):
      ...    registerFormats = ('sonix',)
      ...    registerInputExtensions = ('mus',)
  
      >>> converter.registerSubconverter(ConverterSonix)
      >>> scf = converter.Converter().getSubConverterFormats()
      >>> for x in sorted(scf):
      ...     x, scf[x]
      ('abc', <class 'music21.converter.subConverters.ConverterABC'>)
      ...
      ('sonix', <class 'music21.ConverterSonix'>)
      ...
  
      See `converter.qmConverter` for an example of an extended subconverter.
  
      >>> converter.resetSubconverters() #_DOCS_HIDE
  
      '''
      _registeredSubconverters.append(newSubConverter)
  
  
  def unregisterSubconverter(removeSubconverter):
      # noinspection PyShadowingNames
      '''
      Remove a Subconverter from the list of registered subconverters.
  
      >>> converter.resetSubconverters() #_DOCS_HIDE
      >>> mxlConverter = converter.subConverters.ConverterMusicXML
  
      >>> c = converter.Converter()
      >>> mxlConverter in c.subconvertersList()
      True
      >>> converter.unregisterSubconverter(mxlConverter)
      >>> mxlConverter in c.subconvertersList()
      False
  
      if there is no such subConverter registered and it is not a default subconverter,
      then a converter.ConverterException is raised:
  
      >>> class ConverterSonix(converter.subConverters.SubConverter):
      ...    registerFormats = ('sonix',)
      ...    registerInputExtensions = ('mus',)
      >>> converter.unregisterSubconverter(ConverterSonix)
      Traceback (most recent call last):
      music21.converter.ConverterException: Could not remove <class 'music21.ConverterSonix'> from
                  registered subconverters
  
      The special command "all" removes everything including the default converters:
  
      >>> converter.unregisterSubconverter('all')
      >>> c.subconvertersList()
      []
  
      >>> converter.resetSubconverters() #_DOCS_HIDE
  
      '''
      global _registeredSubconverters  # pylint: disable=global-statement
      global _deregisteredSubconverters  # pylint: disable=global-statement
      if removeSubconverter == 'all':
          _registeredSubconverters = []
          _deregisteredSubconverters = ['all']
          return
  
      try:
          _registeredSubconverters.remove(removeSubconverter)
      except ValueError:
          c = Converter()
          dsc = c.defaultSubconverters()
          if removeSubconverter in dsc:
              _deregisteredSubconverters.append(removeSubconverter)
          else:
              raise ConverterException(
                  f'Could not remove {removeSubconverter!r} from registered subconverters')
  
  
  # ------------------------------------------------------------------------------
  
  
  class Converter:
      '''
      A class used for converting all supported data formats into music21 objects.
  
      Not a subclass, but a wrapper for different converter objects based on format.
      '''
      _DOC_ATTR = {'subConverter': 'a ConverterXXX object that will do the actual converting.', }
  
      def __init__(self):
          self.subConverter = None
          self._thawedStream = None  # a stream object unthawed
  
      def _getDownloadFp(self, directory, ext, url):
          if directory is None:
              raise ValueError('Directory must be provided')
  
          if isinstance(directory, str):
              directory = pathlib.Path(directory)
  
          filename = 'm21-' + _version.__version__ + '-' + common.getMd5(url) + ext
          return directory / filename
  
      # pylint: disable=redefined-builtin
      # noinspection PyShadowingBuiltins
      def parseFileNoPickle(self, fp, number=None,
                            format=None, forceSource=False, **keywords):  # @ReservedAssignment
          '''
          Given a file path, parse and store a music21 Stream.
  
          If format is None then look up the format from the file
          extension using `common.findFormatFile`.
  
          Does not use or store pickles in any circumstance.
          '''
          fp = common.cleanpath(fp, returnPathlib=True)
          # environLocal.printDebug(['attempting to parseFile', fp])
          if not fp.exists():
              raise ConverterFileException(f'no such file exists: {fp}')
          useFormat = format
  
          if useFormat is None:
              useFormat = self.getFormatFromFileExtension(fp)
  
          self.setSubconverterFromFormat(useFormat)
          self.subConverter.keywords = keywords
          try:
              self.subConverter.parseFile(fp, number=number, **keywords)
          except NotImplementedError:
              raise ConverterFileException(f'File is not in a correct format: {fp}')
  
          self.stream.filePath = str(fp)
          self.stream.fileNumber = number
          self.stream.fileFormat = useFormat
  
      def getFormatFromFileExtension(self, fp):
          # noinspection PyShadowingNames
          '''
          gets the format from a file extension.
  
          >>> fp = common.getSourceFilePath() / 'musedata' / 'testZip.zip'
          >>> c = converter.Converter()
          >>> c.getFormatFromFileExtension(fp)
          'musedata'
          '''
          fp = common.cleanpath(fp, returnPathlib=True)
          # if the file path is to a directory, assume it is a collection of
          # musedata parts
          useFormat = None
          if fp.is_dir():
              useFormat = 'musedata'
          else:
              useFormat = common.findFormatFile(fp)
              if useFormat is None:
                  raise ConverterFileException(f'cannot find a format extensions for: {fp}')
          return useFormat
  
      # noinspection PyShadowingBuiltins
      def parseFile(self, fp, number=None,
                    format=None, forceSource=False, storePickle=True, **keywords):
          '''
          Given a file path, parse and store a music21 Stream.
  
          If format is None then look up the format from the file
          extension using `common.findFormatFile`.
  
          Will load from a pickle unless forceSource is True
          Will store as a pickle unless storePickle is False
          '''
          from music21 import freezeThaw
          fp = common.cleanpath(fp, returnPathlib=True)
          if not fp.exists():
              raise ConverterFileException(f'no such file exists: {fp}')
          useFormat = format
  
          if useFormat is None:
              useFormat = self.getFormatFromFileExtension(fp)
  
          pfObj = PickleFilter(fp, forceSource, number, **keywords)
          unused_fpDst, writePickle, fpPickle = pfObj.status()
          if writePickle is False and fpPickle is not None and forceSource is False:
              environLocal.printDebug('Loading Pickled version')
              try:
                  self._thawedStream = thaw(fpPickle, zipType='zlib')
              except freezeThaw.FreezeThawException:
                  environLocal.warn(f'Could not parse pickle, {fpPickle} ...rewriting')
                  os.remove(fpPickle)
                  self.parseFileNoPickle(fp, number, format, forceSource, **keywords)
  
              self.stream.filePath = fp
              self.stream.fileNumber = number
              self.stream.fileFormat = useFormat
          else:
              environLocal.printDebug('Loading original version')
              self.parseFileNoPickle(fp, number, format, forceSource, **keywords)
              if writePickle is True and fpPickle is not None and storePickle is True:
                  # save the stream to disk...
                  environLocal.printDebug('Freezing Pickle')
                  s = self.stream
                  sf = freezeThaw.StreamFreezer(s, fastButUnsafe=True)
                  sf.write(fp=fpPickle, zipType='zlib')
  
                  environLocal.printDebug('Replacing self.stream')
                  # get a new stream
                  self._thawedStream = thaw(fpPickle, zipType='zlib')
                  self.stream.filePath = fp
                  self.stream.fileNumber = number
                  self.stream.fileFormat = useFormat
  
      def parseData(self, dataStr, number=None,
                    format=None, forceSource=False, **keywords):  # @ReservedAssignment
          '''
          Given raw data, determine format and parse into a music21 Stream.
          '''
          useFormat = format
          # get from data in string if not specified
          if useFormat is None:  # it's a string
              dataStr = dataStr.lstrip()
              useFormat, dataStr = self.formatFromHeader(dataStr)
  
              if isinstance(dataStr, bytes):
                  dataStrMakeStr = dataStr.decode('utf-8', 'ignore')
              else:
                  dataStrMakeStr = dataStr
  
              if useFormat is not None:
                  pass
              elif dataStrMakeStr.startswith('<?xml'):
                  # is it MEI or MusicXML?
                  if '<mei' in dataStrMakeStr:
                      useFormat = 'mei'
                  else:
                      useFormat = 'musicxml'
              elif dataStrMakeStr.startswith('mei:') or dataStrMakeStr.lower().startswith('mei:'):
                  useFormat = 'mei'
              elif dataStrMakeStr.lower().startswith('musicxml:'):
                  useFormat = 'musicxml'
              elif dataStrMakeStr.startswith('MThd') or dataStrMakeStr.lower().startswith('midi:'):
                  useFormat = 'midi'
              elif (dataStrMakeStr.startswith('!!!')
                      or dataStrMakeStr.startswith('**')
                      or dataStrMakeStr.lower().startswith('humdrum:')):
                  useFormat = 'humdrum'
              elif dataStrMakeStr.lower().startswith('tinynotation:'):
                  useFormat = 'tinyNotation'
  
              # assume MuseData must define a meter and a key
              elif 'WK#:' in dataStrMakeStr and 'measure' in dataStrMakeStr:
                  useFormat = 'musedata'
              elif 'M:' in dataStrMakeStr and 'K:' in dataStrMakeStr:
                  useFormat = 'abc'
              elif 'Time Signature:' in dataStrMakeStr and 'm1' in dataStrMakeStr:
                  useFormat = 'romanText'
              else:
                  raise ConverterException('File not found or no such format found for: %s' %
                                           dataStrMakeStr)
  
          self.setSubconverterFromFormat(useFormat)
          self.subConverter.keywords = keywords
          self.subConverter.parseData(dataStr, number=number)
  
      def parseURL(self, url, format=None, number=None, **keywords):  # @ReservedAssignment
          '''Given a url, download and parse the file
          into a music21 Stream stored in the `stream`
          property of the converter object.
  
          Note that this checks the user Environment
          `autoDownload` setting before downloading.
  
          >>> jeanieLightBrownURL = ('https://github.com/cuthbertLab/music21/raw/master' +
          ...        '/music21/corpus/leadSheet/fosterBrownHair.mxl')
          >>> c = converter.Converter()
          >>> #_DOCS_SHOW c.parseURL(jeanieLightBrownURL)
          >>> #_DOCS_SHOW jeanieStream = c.stream
          '''
          autoDownload = environLocal['autoDownload']
          if autoDownload in ('deny', 'ask'):
              message = 'Automatic downloading of URLs is presently set to {!r};'
              message += ' configure your Environment "autoDownload" setting to '
              message += '"allow" to permit automatic downloading: '
              message += "environment.set('autoDownload', 'allow')"
              message = message.format(autoDownload)
              raise ConverterException(message)
  
          # this format check is here first to see if we can find the format
          # in the url; if forcing a format we do not need this
          # we do need the file extension to construct file path below
          if format is None:
              formatFromURL, ext = common.findFormatExtURL(url)
              if formatFromURL is None:  # cannot figure out what it is
                  raise ConverterException(f'cannot determine file format of url: {url}')
          else:
              unused_formatType, ext = common.findFormat(format)
              if ext is None:
                  ext = '.txt'
  
          directory = environLocal.getRootTempDir()
          dst = self._getDownloadFp(directory, ext, url)  # returns pathlib.Path
          urlretrieve = urllib.request.urlretrieve
  
          if not dst.exists():
              try:
                  environLocal.printDebug(['downloading to:', str(dst)])
                  fp, unused_headers = urlretrieve(url, filename=str(dst))
              except IOError:
                  raise ConverterException(f'cannot access file: {url}')
          else:
              environLocal.printDebug(['using already downloaded file:', str(dst)])
              fp = dst
  
          # update format based on downloaded fp
          if format is None:  # if not provided as an argument
              useFormat = common.findFormatFile(fp)
          else:
              useFormat = format
          self.setSubconverterFromFormat(useFormat)
          self.subConverter.keywords = keywords
          self.subConverter.parseFile(fp, number=number)
          self.stream.filePath = fp
          self.stream.fileNumber = number
          self.stream.fileFormat = useFormat
  
      # -----------------------------------------------------------------------#
      # Subconverters
      def subconvertersList(self, converterType='any'):
          '''
          Gives a list of all the subconverters that are registered.
  
          If converterType is 'any' (true), then input or output
          subconverters are listed.
  
          Otherwise, 'input', or 'output' can be used to filter.
  
          >>> converter.resetSubconverters() #_DOCS_HIDE
          >>> c = converter.Converter()
          >>> scl = c.subconvertersList()
          >>> defaultScl = c.defaultSubconverters()
          >>> tuple(scl) == tuple(defaultScl)
          True
  
          >>> sclInput = c.subconvertersList('input')
          >>> sclInput
          [<class 'music21.converter.subConverters.ConverterABC'>,
           <class 'music21.converter.subConverters.ConverterCapella'>,
           <class 'music21.converter.subConverters.ConverterClercqTemperley'>,
           <class 'music21.converter.subConverters.ConverterHumdrum'>,
           <class 'music21.converter.subConverters.ConverterMEI'>,
           <class 'music21.converter.subConverters.ConverterMidi'>,
           <class 'music21.converter.subConverters.ConverterMuseData'>,
           <class 'music21.converter.subConverters.ConverterMusicXML'>,
           <class 'music21.converter.subConverters.ConverterNoteworthy'>,
           <class 'music21.converter.subConverters.ConverterNoteworthyBinary'>,
           <class 'music21.converter.subConverters.ConverterRomanText'>,
           <class 'music21.converter.subConverters.ConverterScala'>,
           <class 'music21.converter.subConverters.ConverterTinyNotation'>,
           <class 'music21.converter.subConverters.ConverterVolpiano'>]
  
          >>> sclOutput = c.subconvertersList('output')
          >>> sclOutput
          [<class 'music21.converter.subConverters.ConverterBraille'>,
           <class 'music21.converter.subConverters.ConverterLilypond'>,
           <class 'music21.converter.subConverters.ConverterMidi'>,
           <class 'music21.converter.subConverters.ConverterMusicXML'>,
           <class 'music21.converter.subConverters.ConverterRomanText'>,
           <class 'music21.converter.subConverters.ConverterScala'>,
           <class 'music21.converter.subConverters.ConverterText'>,
           <class 'music21.converter.subConverters.ConverterTextLine'>,
           <class 'music21.converter.subConverters.ConverterVexflow'>,
           <class 'music21.converter.subConverters.ConverterVolpiano'>]
  
  
  
          >>> class ConverterSonix(converter.subConverters.SubConverter):
          ...    registerFormats = ('sonix',)
          ...    registerInputExtensions = ('mus',)
          >>> converter.registerSubconverter(ConverterSonix)
          >>> ConverterSonix in c.subconvertersList()
          True
  
          >>> converter.resetSubconverters() #_DOCS_HIDE
          '''
          subConverterList = []
          for reg in _registeredSubconverters:
              # print(reg)
              subConverterList.append(reg)
  
          if _deregisteredSubconverters and _deregisteredSubconverters[0] == 'all':
              pass
          else:
              subConverterList.extend(self.defaultSubconverters())
              for unregistered in _deregisteredSubconverters:
                  try:
                      subConverterList.remove(unregistered)
                  except ValueError:
                      pass
  
          if converterType == 'any':
              return subConverterList
  
          filteredSubConvertersList = []
          for sc in subConverterList:
              if converterType == 'input' and not sc.registerInputExtensions:
                  continue
              if converterType == 'output' and not sc.registerOutputExtensions:
                  continue
              filteredSubConvertersList.append(sc)
  
          return filteredSubConvertersList
  
      def defaultSubconverters(self):
          '''
          return an alphabetical list of the default subconverters: those in converter.subConverters
          with the class Subconverter.
  
          Do not use generally.  use c.subConvertersList()
  
          >>> c = converter.Converter()
          >>> for sc in c.defaultSubconverters():
          ...     print(sc)
          <class 'music21.converter.subConverters.ConverterABC'>
          <class 'music21.converter.subConverters.ConverterBraille'>
          <class 'music21.converter.subConverters.ConverterCapella'>
          <class 'music21.converter.subConverters.ConverterClercqTemperley'>
          <class 'music21.converter.subConverters.ConverterHumdrum'>
          <class 'music21.converter.subConverters.ConverterIPython'>
          <class 'music21.converter.subConverters.ConverterLilypond'>
          <class 'music21.converter.subConverters.ConverterMEI'>
          <class 'music21.converter.subConverters.ConverterMidi'>
          <class 'music21.converter.subConverters.ConverterMuseData'>
          <class 'music21.converter.subConverters.ConverterMusicXML'>
          <class 'music21.converter.subConverters.ConverterNoteworthy'>
          <class 'music21.converter.subConverters.ConverterNoteworthyBinary'>
          <class 'music21.converter.subConverters.ConverterRomanText'>
          <class 'music21.converter.subConverters.ConverterScala'>
          <class 'music21.converter.subConverters.ConverterText'>
          <class 'music21.converter.subConverters.ConverterTextLine'>
          <class 'music21.converter.subConverters.ConverterTinyNotation'>
          <class 'music21.converter.subConverters.ConverterVexflow'>
          <class 'music21.converter.subConverters.ConverterVolpiano'>
          <class 'music21.converter.subConverters.SubConverter'>
          '''
          defaultSubconverters = []
          for i in sorted(subConverters.__dict__):
              name = getattr(subConverters, i)
              # noinspection PyTypeChecker
              if (callable(name)
                      and not isinstance(name, types.FunctionType)
                      and hasattr(name, '__mro__')   # Typing imports break this.
                      and subConverters.SubConverter in name.__mro__):
                  defaultSubconverters.append(name)
          return defaultSubconverters
  
      def getSubConverterFormats(self):
          '''
          Get a dictionary of subConverters for various formats.
  
          >>> scf = converter.Converter().getSubConverterFormats()
          >>> scf['abc']
          <class 'music21.converter.subConverters.ConverterABC'>
          >>> for x in sorted(scf):
          ...     x, scf[x]
          ('abc', <class 'music21.converter.subConverters.ConverterABC'>)
          ('braille', <class 'music21.converter.subConverters.ConverterBraille'>)
          ('capella', <class 'music21.converter.subConverters.ConverterCapella'>)
          ('cttxt', <class 'music21.converter.subConverters.ConverterClercqTemperley'>)
          ('har', <class 'music21.converter.subConverters.ConverterClercqTemperley'>)
          ('humdrum', <class 'music21.converter.subConverters.ConverterHumdrum'>)
          ('ipython', <class 'music21.converter.subConverters.ConverterIPython'>)
          ('lily', <class 'music21.converter.subConverters.ConverterLilypond'>)
          ('lilypond', <class 'music21.converter.subConverters.ConverterLilypond'>)
          ('mei', <class 'music21.converter.subConverters.ConverterMEI'>)
          ('midi', <class 'music21.converter.subConverters.ConverterMidi'>)
          ('musedata', <class 'music21.converter.subConverters.ConverterMuseData'>)
          ('musicxml', <class 'music21.converter.subConverters.ConverterMusicXML'>)
          ('noteworthy', <class 'music21.converter.subConverters.ConverterNoteworthyBinary'>)
          ('noteworthytext', <class 'music21.converter.subConverters.ConverterNoteworthy'>)
          ('rntext', <class 'music21.converter.subConverters.ConverterRomanText'>)
          ('romantext', <class 'music21.converter.subConverters.ConverterRomanText'>)
          ('scala', <class 'music21.converter.subConverters.ConverterScala'>)
          ('t', <class 'music21.converter.subConverters.ConverterText'>)
          ('text', <class 'music21.converter.subConverters.ConverterText'>)
          ('textline', <class 'music21.converter.subConverters.ConverterTextLine'>)
          ('tinynotation', <class 'music21.converter.subConverters.ConverterTinyNotation'>)
          ('txt', <class 'music21.converter.subConverters.ConverterText'>)
          ('vexflow', <class 'music21.converter.subConverters.ConverterVexflow'>)
          ('volpiano', <class 'music21.converter.subConverters.ConverterVolpiano'>)
          ('xml', <class 'music21.converter.subConverters.ConverterMusicXML'>)
          '''
          converterFormats = {}
          for name in self.subconvertersList():
              if hasattr(name, 'registerFormats'):
                  formatsTuple = name.registerFormats
                  for f in formatsTuple:
                      converterFormats[f.lower()] = name
          return converterFormats
  
      def setSubconverterFromFormat(self, converterFormat):
          '''
          sets the .subConverter according to the format of `converterFormat`:
  
          >>> convObj = converter.Converter()
          >>> convObj.setSubconverterFromFormat('humdrum')
          >>> convObj.subConverter
          <music21.converter.subConverters.ConverterHumdrum object at 0x...>
          '''
          if converterFormat is None:
              raise ConverterException('Did not find a format from the source file')
          converterFormat = converterFormat.lower()
          scf = self.getSubConverterFormats()
          if converterFormat not in scf:
              raise ConverterException(f'no converter available for format: {converterFormat}')
          subConverterClass = scf[converterFormat]
          self.subConverter = subConverterClass()
  
      def formatFromHeader(self, dataStr):
          '''
          if dataStr begins with a text header such as  "tinyNotation:" then
          return that format plus the dataStr with the head removed.
  
          Else, return (None, dataStr) where dataStr is the original untouched.
  
          Not case sensitive.
  
          >>> c = converter.Converter()
          >>> c.formatFromHeader('tinynotation: C4 E2')
          ('tinynotation', 'C4 E2')
  
          >>> c.formatFromHeader('C4 E2')
          (None, 'C4 E2')
  
          >>> c.formatFromHeader('romanText: m1: a: I b2 V')
          ('romantext', 'm1: a: I b2 V')
  
          New formats can register new headers:
  
          >>> class ConverterSonix(converter.subConverters.SubConverter):
          ...    registerFormats = ('sonix',)
          ...    registerInputExtensions = ('mus',)
          >>> converter.registerSubconverter(ConverterSonix)
          >>> c.formatFromHeader('sonix: AIFF data')
          ('sonix', 'AIFF data')
          >>> converter.resetSubconverters() #_DOCS_HIDE
          '''
          dataStrStartLower = dataStr[:20].lower()
          if isinstance(dataStrStartLower, bytes):
              dataStrStartLower = dataStrStartLower.decode('utf-8', 'ignore')
  
          foundFormat = None
          subconverterList = self.subconvertersList()
          for sc in subconverterList:
              for possibleFormat in sc.registerFormats:
                  if dataStrStartLower.startswith(possibleFormat.lower() + ':'):
                      foundFormat = possibleFormat
                      dataStr = dataStr[len(foundFormat) + 1:]
                      dataStr = dataStr.lstrip()
                      break
          return (foundFormat, dataStr)
  
      def regularizeFormat(self, fmt):
          '''
          Take in a string representing a format, a file extension (w/ or without leading dot)
          etc. and find the format string that best represents the format that should be used.
  
          Searches SubConverter.registerFormats first, then SubConverter.registerInputExtensions,
          then SubConverter.registerOutputExtensions
  
          Returns None if no format applies:
  
          >>> c = converter.Converter()
          >>> c.regularizeFormat('mxl')
          'musicxml'
          >>> c.regularizeFormat('t')
          'text'
          >>> c.regularizeFormat('abc')
          'abc'
          >>> c.regularizeFormat('lily.png')
          'lilypond'
          >>> c.regularizeFormat('blah') is None
          True
          '''
          # make lower case, as some lilypond processing used upper case
          fmt = fmt.lower().strip()
          if fmt.startswith('.'):
              fmt = fmt[1:]  # strip .
          foundSc = None
  
          formatList = fmt.split('.')
          fmt = formatList[0]
          if len(formatList) > 1:
              unused_subformats = formatList[1:]
          else:
              unused_subformats = []
          scl = self.subconvertersList()
  
          for sc in scl:
              formats = sc.registerFormats
              for scFormat in formats:
                  if fmt == scFormat:
                      foundSc = sc
                      break
              if foundSc is not None:
                  break
  
          if foundSc is None:
              for sc in scl:
                  extensions = sc.registerInputExtensions
                  for ext in extensions:
                      if fmt == ext:
                          foundSc = sc
                          break
                  if foundSc is not None:
                      break
          if foundSc is None:
              for sc in scl:
                  extensions = sc.registerInputExtensions
                  for ext in extensions:
                      if fmt == ext:
                          foundSc = sc
                          break
                  if foundSc is not None:
                      break
  
          if foundSc and foundSc.registerFormats:
              return foundSc.registerFormats[0]
          else:
              return None
  
      # --------------------------------------------------------------------------
      # properties
      @property
      def stream(self):
          '''
          Returns the .subConverter.stream object.
          '''
          if self._thawedStream is not None:
              return self._thawedStream
          elif self.subConverter is not None:
              return self.subConverter.stream
          else:
              return None
          # not _stream: please don't look in other objects' private variables;
          #              humdrum worked differently.
  
  
  # ------------------------------------------------------------------------------
  # module level convenience methods
  
  # pylint: disable=redefined-builtin
  # noinspection PyShadowingBuiltins
  def parseFile(fp, number=None, format=None, forceSource=False, **keywords):  # @ReservedAssignment
      '''
      Given a file path, attempt to parse the file into a Stream.
      '''
      v = Converter()
      fp = common.cleanpath(fp, returnPathlib=True)
      v.parseFile(fp, number=number, format=format, forceSource=forceSource, **keywords)
      return v.stream
  
  # pylint: disable=redefined-builtin
  # noinspection PyShadowingBuiltins
  def parseData(dataStr, number=None, format=None, **keywords):  # @ReservedAssignment
      '''
      Given musical data represented within a Python string, attempt to parse the
      data into a Stream.
      '''
      v = Converter()
      v.parseData(dataStr, number=number, format=format, **keywords)
      return v.stream
  
  # pylint: disable=redefined-builtin
  # noinspection PyShadowingBuiltins
  def parseURL(url, number=None, format=None, forceSource=False, **keywords):  # @ReservedAssignment
      '''
      Given a URL, attempt to download and parse the file into a Stream. Note:
      URL downloading will not happen automatically unless the user has set their
      Environment "autoDownload" preference to "allow".
      '''
      v = Converter()
      v.parseURL(url, format=format, **keywords)
      return v.stream
  
  
  def parse(value: Union[bundles.MetadataEntry, bytes, str, pathlib.Path],
            *args,
            **keywords) -> 'music21.stream.Stream':
      r'''
      Given a file path, encoded data in a Python string, or a URL, attempt to
      parse the item into a Stream.  Note: URL downloading will not happen
      automatically unless the user has set their Environment "autoDownload"
      preference to "allow".
  
      Keywords can include `number` which specifies a piece number in a file of
      multi-piece file.
  
      `format` specifies the format to parse the line of text or the file as.
  
      `quantizePost` specifies whether to quantize a stream resulting from MIDI conversion.
      By default, MIDI streams are quantized to the nearest sixteenth or triplet-eighth
      (i.e. smaller durations will not be preserved).
      `quarterLengthDivisors` sets the quantization units explicitly.
  
      A string of text is first checked to see if it is a filename that exists on
      disk.  If not it is searched to see if it looks like a URL.  If not it is
      processed as data.
  
      PC File:
  
      >>> #_DOCS_SHOW s = converter.parse(r'c:\users\myke\desktop\myFile.xml')
  
      Mac File:
  
      >>> #_DOCS_SHOW s = converter.parse('/Users/cuthbert/Desktop/myFile.xml')
  
      URL:
  
      >>> #_DOCS_SHOW s = converter.parse('http://midirepository.org/file220/file.mid')
  
  
      Data is preceded by an identifier such as "tinynotation:"
  
      >>> s = converter.parse("tinyNotation: 3/4 E4 r f# g=lastG trip{b-8 a g} c", makeNotation=False)
      >>> s.getElementsByClass(meter.TimeSignature)[0]
      <music21.meter.TimeSignature 3/4>
  
      or the format can be passed directly:
  
      >>> s = converter.parse("2/16 E4 r f# g=lastG trip{b-8 a g} c", format='tinyNotation').flat
      >>> s.getElementsByClass(meter.TimeSignature)[0]
      <music21.meter.TimeSignature 2/16>
  
      .. tip::
  
          Unlike musicxml, MIDI files are unmeasured. Call :meth:`~music21.stream.Stream.makeNotation`
          on the parsed result if you wish measures to be created before further manipulation.
  
          Save the result of `makeNotation`, or use `inPlace=True`::
  
              unmeasuredStream = converter.parse('/Users/you/Desktop/source.mid')
              measuredStream = unmeasuredStream.makeNotation()
  
          This is particularly important when writing formats that do not run `makeNotation` as
          a convenience during the export (musicxml does; lilypond doesn't.)
      '''
      # environLocal.printDebug(['attempting to parse()', value])
      if 'forceSource' in keywords:
          forceSource = keywords['forceSource']
          del(keywords['forceSource'])
      else:
          forceSource = False
  
      # see if a work number is defined; for multi-work collections
      if 'number' in keywords:
          number = keywords['number']
          del(keywords['number'])
      else:
          number = None
  
      if 'format' in keywords:
          m21Format = keywords['format']
          del(keywords['format'])
      else:
          m21Format = None
  
      if isinstance(value, bytes):
          valueStr = value.decode('utf-8', 'ignore')
      if isinstance(value, pathlib.Path):
          valueStr = str(value)
      elif isinstance(value, bundles.MetadataEntry):
          if value.sourcePath.is_absolute():
              valueStr = str(value.sourcePath)
          else:
              valueStr = str(common.getCorpusFilePath() / value.sourcePath)
      else:
          valueStr = value
  
      if (common.isListLike(value)
              and len(value) == 2
              and value[1] is None
              and _osCanLoad(str(value[0]))):
          # comes from corpus.search
          return parseFile(value[0], format=m21Format, **keywords)
      elif (common.isListLike(value)
            and len(value) == 2
            and isinstance(value[1], int)
            and _osCanLoad(str(value[0]))):
          # corpus or other file with movement number
          return parseFile(value[0], format=m21Format, **keywords).getScoreByNumber(value[1])
      elif common.isListLike(value) or args:  # tiny notation list. TODO: Remove.
          if args:  # add additional args to a list
              value = [value] + list(args)
          return parseData(value, number=number, **keywords)
      # a midi string, must come before os.path.exists test
      elif not isinstance(value, bytes) and valueStr.startswith('MThd'):
          return parseData(value, number=number, format=m21Format, **keywords)
      elif (not isinstance(value, bytes)
            and _osCanLoad(valueStr)):
          return parseFile(valueStr, number=number, format=m21Format,
                           forceSource=forceSource, **keywords)
      elif (not isinstance(value, bytes)
            and _osCanLoad(common.cleanpath(valueStr))):
          return parseFile(common.cleanpath(valueStr), number=number, format=m21Format,
                           forceSource=forceSource, **keywords)
  
      elif not isinstance(valueStr, bytes) and (valueStr.startswith('http://')
                                                or valueStr.startswith('https://')):
          # it's a url; may need to broaden these criteria
          return parseURL(value, number=number, format=m21Format,
                          forceSource=forceSource, **keywords)
  
      elif isinstance(value, pathlib.Path):
          raise FileNotFoundError(f'Cannot find file in {str(value)}')
      else:
          return parseData(value, number=number, format=m21Format, **keywords)
  
  
  def freeze(streamObj, fmt=None, fp=None, fastButUnsafe=False, zipType='zlib') -> pathlib.Path:
      # noinspection PyShadowingNames
      '''Given a StreamObject and a file path, serialize and store the Stream to a file.
  
      This function is based on the :class:`~music21.converter.StreamFreezer` object.
  
      The serialization format is defined by the `fmt` argument; 'pickle' (the default) is only one
      presently supported.  'json' or 'jsonnative' will be used once jsonpickle is good enough.
  
      If no file path is given, a temporary file is used.
  
      The file path is returned.
  
      >>> c = converter.parse('tinynotation: 4/4 c4 d e f')
      >>> c.show('text')
      {0.0} <music21.stream.Measure 1 offset=0.0>
          {0.0} <music21.clef.TrebleClef>
          {0.0} <music21.meter.TimeSignature 4/4>
          {0.0} <music21.note.Note C>
          {1.0} <music21.note.Note D>
          {2.0} <music21.note.Note E>
          {3.0} <music21.note.Note F>
          {4.0} <music21.bar.Barline type=final>
      >>> fp = converter.freeze(c, fmt='pickle')
      >>> #_DOCS_SHOW fp
      PosixPath('/tmp/music21/sjiwoe.p.gz')
  
      The file can then be "thawed" back into a Stream using the
      :func:`~music21.converter.thaw` method.
  
      >>> d = converter.thaw(fp)
      >>> d.show('text')
      {0.0} <music21.stream.Measure 1 offset=0.0>
          {0.0} <music21.clef.TrebleClef>
          {0.0} <music21.meter.TimeSignature 4/4>
          {0.0} <music21.note.Note C>
          {1.0} <music21.note.Note D>
          {2.0} <music21.note.Note E>
          {3.0} <music21.note.Note F>
          {4.0} <music21.bar.Barline type=final>
      '''
      from music21 import freezeThaw
      v = freezeThaw.StreamFreezer(streamObj, fastButUnsafe=fastButUnsafe)
      return v.write(fmt=fmt, fp=fp, zipType=zipType)  # returns fp
  
  
  def thaw(fp, zipType='zlib'):
      '''Given a file path of a serialized Stream, defrost the file into a Stream.
  
      This function is based on the :class:`~music21.converter.StreamFreezer` object.
  
      See the documentation for :meth:`~music21.converter.freeze` for demos.
      '''
      from music21 import freezeThaw
      v = freezeThaw.StreamThawer()
      v.open(fp, zipType=zipType)
      return v.stream
  
  
  def freezeStr(streamObj, fmt=None):
      '''
      Given a StreamObject
      serialize and return a serialization string.
  
      This function is based on the
      :class:`~music21.converter.StreamFreezer` object.
  
      The serialization format is defined by
      the `fmt` argument; 'pickle' (the default),
      is the only one presently supported.
  
  
      >>> c = converter.parse('tinyNotation: 4/4 c4 d e f', makeNotation=False)
      >>> c.show('text')
      {0.0} <music21.meter.TimeSignature 4/4>
      {0.0} <music21.note.Note C>
      {1.0} <music21.note.Note D>
      {2.0} <music21.note.Note E>
      {3.0} <music21.note.Note F>
      >>> data = converter.freezeStr(c, fmt='pickle')
      >>> len(data) > 20  # pickle implementation dependent
      True
      >>> d = converter.thawStr(data)
      >>> d.show('text')
      {0.0} <music21.meter.TimeSignature 4/4>
      {0.0} <music21.note.Note C>
      {1.0} <music21.note.Note D>
      {2.0} <music21.note.Note E>
      {3.0} <music21.note.Note F>
  
      '''
      from music21 import freezeThaw
      v = freezeThaw.StreamFreezer(streamObj)
      return v.writeStr(fmt=fmt)  # returns a string
  
  
  def thawStr(strData):
      '''
      Given a serialization string, defrost into a Stream.
  
      This function is based on the :class:`~music21.converter.StreamFreezer` object.
      '''
      from music21 import freezeThaw
      v = freezeThaw.StreamThawer()
      v.openStr(strData)
      return v.stream
  
  
  def _osCanLoad(fp: str) -> bool:
      '''
      Return os.path.exists, but catch `ValueError` and return False.
  
      os.path.exists raises ValueError for paths over 260 chars
      on all versions of Windows lacking the `LongPathsEnabled` setting,
      which is absent below Windows 10 v.1607 and opt-in on higher versions.
      '''
      try:
          return os.path.exists(fp)
      except ValueError:  # pragma: no cover
          return False
  
  
  # ------------------------------------------------------------------------------
  class TestExternal(unittest.TestCase):  # pragma: no cover
      # interpreter loading
  
      def testMusicXMLConversion(self):
          from music21.musicxml import testFiles
          for mxString in testFiles.ALL:  # @UndefinedVariable
              a = subConverters.ConverterMusicXML()
              a.parseData(mxString)
  
      def testMusicXMLTabConversion(self):
          from music21.musicxml import testFiles
  
          mxString = testFiles.ALL[5]  # @UndefinedVariable
          a = subConverters.ConverterMusicXML()
          a.parseData(mxString)
  
          b = parseData(mxString)
          b.show('text')
  
          # {0.0} <music21.metadata.Metadata object at 0x04501CD0>
          # {0.0} <music21.stream.Part Electric Guitar>
          #    {0.0} <music21.instrument.Instrument P0: Electric Guitar: >
          #    {0.0} <music21.stream.Measure 0 offset=0.0>
          #        {0.0} <music21.layout.StaffLayout distance None, ...staffLines 6>
          #        {0.0} <music21.clef.TabClef>
          #        {0.0} <music21.tempo.MetronomeMark animato Quarter=120.0>
          #        {0.0} <music21.key.KeySignature of no sharps or flats, mode major>
          #        {0.0} <music21.meter.TimeSignature 4/4>
          #        {0.0} <music21.note.Note F>
          #        {2.0} <music21.note.Note F#>
  
          b.show()
          pass
  
      def testConversionMusicXml(self):
          c = stream.Score()
  
          from music21.musicxml import testPrimitive
          mxString = testPrimitive.chordsThreeNotesDuration21c
          a = parseData(mxString)
  
          mxString = testPrimitive.beams01
          b = parseData(mxString)
          # b.show()
  
          c.append(a[0])
          c.append(b[0])
          c.show()
          # TODO: this is only showing the minimum number of measures
  
      def testParseURL(self):
          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
  
      def testFreezer(self):
          from music21 import corpus
          s = corpus.parse('bach/bwv66.6.xml')
          fp = freeze(s)
          s2 = thaw(fp)
          s2.show()
  
  
  class Test(unittest.TestCase):
  
      def testCopyAndDeepcopy(self):
          '''Test copying all objects defined in this module
          '''
          for part in sys.modules[self.__module__].__dict__:
              match = False
              for skip in ['_', '__', 'Test', 'Exception']:
                  if part.startswith(skip) or part.endswith(skip):
                      match = True
              if match:
                  continue
              obj = getattr(sys.modules[self.__module__], part)
              # noinspection PyTypeChecker
              if callable(obj) and not isinstance(obj, types.FunctionType):
                  i = copy.copy(obj)
                  j = copy.deepcopy(obj)
  
      def testConversionMX(self):
          from music21.musicxml import testPrimitive
          from music21 import dynamics
          from music21 import note
  
          mxString = testPrimitive.pitches01a
          a = parse(mxString)
          a = a.flat
          b = a.getElementsByClass(note.Note)
          # there should be 102 notes
          self.assertEqual(len(b), 102)
  
          # test directions, dynamics, wedges
          mxString = testPrimitive.directions31a
          a = parse(mxString)
          a = a.flat
          b = a.getElementsByClass(dynamics.Dynamic)
          # there should be 27 dynamics found in this file
          self.assertEqual(len(b), 27)
          c = a.getElementsByClass(note.Note)
          self.assertEqual(len(c), 53)
  
          # two starts and two stops == 2!
          d = a.getElementsByClass(dynamics.DynamicWedge)
          self.assertEqual(len(d), 2)
  
          # test lyrics
          mxString = testPrimitive.lyricsMelisma61d
          a = parse(mxString)
          a = a.flat
          b = a.getElementsByClass(note.Note)
          found = []
          for noteObj in b:
              for obj in noteObj.lyrics:
                  found.append(obj)
          self.assertEqual(len(found), 3)
  
          # test we are getting rests
          mxString = testPrimitive.restsDurations02a
          a = parse(mxString)
          a = a.flat
          b = a.getElementsByClass(note.Rest)
          self.assertEqual(len(b), 19)
  
          # test if we can get trills
          mxString = testPrimitive.notations32a
          a = parse(mxString)
          a = a.flat
          b = a.getElementsByClass(note.Note)
  
          mxString = testPrimitive.rhythmDurations03a
          a = parse(mxString)
          # a.show('t')
          self.assertEqual(len(a), 2)  # one part, plus metadata
          for part in a.getElementsByClass(stream.Part):
              self.assertEqual(len(part), 7)  # seven measures
              measures = part.getElementsByClass(stream.Measure)
              self.assertEqual(int(measures[0].number), 1)
              self.assertEqual(int(measures[-1].number), 7)
  
          # print(a.recurseRepr())
  
          # print(a.recurseRepr())
  
          # # get the third movement
          # mxFile = corpus.getWork('opus18no1')[2]
          # a = parse(mxFile)
          # a = a.flat
          # b = a.getElementsByClass(dynamics.Dynamic)
          # # 110 dynamics
          # self.assertEqual(len(b), 110)
          #
          # c = a.getElementsByClass(note.Note)
          # # over 1000 notes
          # self.assertEqual(len(c), 1289)
  
      def testConversionMXChords(self):
          from music21 import chord
          from music21.musicxml import testPrimitive
  
          mxString = testPrimitive.chordsThreeNotesDuration21c
          a = parse(mxString)
          for part in a.getElementsByClass(stream.Part):
              chords = part.flat.getElementsByClass(chord.Chord)
              self.assertEqual(len(chords), 7)
              knownSize = [3, 2, 3, 3, 3, 3, 3]
              for i in range(len(knownSize)):
                  # print(chords[i].pitches, len(chords[i].pitches))
                  self.assertEqual(knownSize[i], len(chords[i].pitches))
  
      def testConversionMXBeams(self):
  
          from music21.musicxml import testPrimitive
  
          mxString = testPrimitive.beams01
          a = parse(mxString)
          part = a.parts[0]
          notes = part.flat.notesAndRests
          beams = []
          for n in notes:
              if 'Note' in n.classes:
                  beams += n.beams.beamsList
          self.assertEqual(len(beams), 152)
  
      def testConversionMXTime(self):
  
          from music21.musicxml import testPrimitive
  
          mxString = testPrimitive.timeSignatures11c
          a = parse(mxString)
          unused_part = a.parts[0]
  
          mxString = testPrimitive.timeSignatures11d
          a = parse(mxString)
          part = a.parts[0]
  
          notes = part.flat.notesAndRests
          self.assertEqual(len(notes), 11)
  
      def testConversionMXClefPrimitive(self):
          from music21 import clef
          from music21.musicxml import testPrimitive
          mxString = testPrimitive.clefs12a
          a = parse(mxString)
          part = a.parts[0]
  
          clefs = part.flat.getElementsByClass(clef.Clef)
          self.assertEqual(len(clefs), 18)
  
      def testConversionMXClefTimeCorpus(self):
  
          from music21 import corpus, clef, meter
          a = corpus.parse('luca')
  
          # there should be only one clef in each part
          clefs = a.parts[0].flat.getElementsByClass(clef.Clef)
          self.assertEqual(len(clefs), 1)
          self.assertEqual(clefs[0].sign, 'G')
  
          # second part
          clefs = a.parts[1].flat.getElementsByClass(clef.Clef)
          self.assertEqual(len(clefs), 1)
          self.assertEqual(clefs[0].octaveChange, -1)
          self.assertEqual(type(clefs[0]).__name__, 'Treble8vbClef')
  
          # third part
          clefs = a.parts[2].flat.getElementsByClass(clef.Clef)
          self.assertEqual(len(clefs), 1)
  
          # check time signature count
          ts = a.parts[1].flat.getElementsByClass(meter.TimeSignature)
          self.assertEqual(len(ts), 4)
  
      def testConversionMXArticulations(self):
          from music21 import note
          from music21.musicxml import testPrimitive
  
          mxString = testPrimitive.articulations01
          a = parse(mxString)
          part = a.parts[0]
  
          notes = part.flat.getElementsByClass(note.Note)
          self.assertEqual(len(notes), 4)
          post = []
          match = ["<class 'music21.articulations.Staccatissimo'>",
                   "<class 'music21.articulations.Accent'>",
                   "<class 'music21.articulations.Staccato'>",
                   "<class 'music21.articulations.Tenuto'>"]
          for i in range(len(notes)):
              post.append(str(notes[i].articulations[0].__class__))
          self.assertEqual(post, match)
          # a.show()
  
      def testConversionMXKey(self):
          from music21 import key
          from music21.musicxml import testPrimitive
          mxString = testPrimitive.keySignatures13a
          a = parse(mxString)
          part = a.parts[0]
  
          keyList = part.flat.getElementsByClass(key.KeySignature)
          self.assertEqual(len(keyList), 46)
  
      def testConversionMXMetadata(self):
          from music21.musicxml import testFiles
  
          a = parse(testFiles.mozartTrioK581Excerpt)  # @UndefinedVariable
          self.assertEqual(a.metadata.composer, 'Wolfgang Amadeus Mozart')
          self.assertEqual(a.metadata.title, 'Quintet for Clarinet and Strings')
          self.assertEqual(a.metadata.movementName, 'Menuetto (Excerpt from Second Trio)')
  
          a = parse(testFiles.binchoisMagnificat)  # @UndefinedVariable
          self.assertEqual(a.metadata.composer, 'Gilles Binchois')
          # this gets the best title available, even though this is movement title
          self.assertEqual(a.metadata.title, 'Excerpt from Magnificat secundi toni')
  
      def testConversionMXBarlines(self):
          from music21 import bar
          from music21.musicxml import testPrimitive
          a = parse(testPrimitive.barlines46a)
          part = a.parts[0]
          barlineList = part.flat.getElementsByClass(bar.Barline)
          self.assertEqual(len(barlineList), 11)
  
      def testConversionXMLayout(self):
  
          from music21.musicxml import testPrimitive
          from music21 import layout
  
          a = parse(testPrimitive.systemLayoutTwoPart)
          # a.show()
  
          part = a.getElementsByClass(stream.Part)[0]
          systemLayoutList = part.flat.getElementsByClass(layout.SystemLayout)
          measuresWithSL = []
          for e in systemLayoutList:
              measuresWithSL.append(e.measureNumber)
          self.assertEqual(measuresWithSL, [1, 3, 4, 5, 7, 8])
          self.assertEqual(len(systemLayoutList), 6)
  
      def testConversionMXTies(self):
  
          from music21.musicxml import testPrimitive
          from music21 import clef
  
          a = parse(testPrimitive.multiMeasureTies)
          # a.show()
  
          countTies = 0
          countStartTies = 0
          for p in a.parts:
              post = p.recurse().notes[0].getContextByClass('Clef')
              self.assertIsInstance(post, clef.TenorClef)
              for n in p.flat.notes:
                  if n.tie is not None:
                      countTies += 1
                      if n.tie.type == 'start' or n.tie.type == 'continue':
                          countStartTies += 1
  
          self.assertEqual(countTies, 57)
          self.assertEqual(countStartTies, 40)
  
      def testConversionMXInstrument(self):
          from music21 import corpus
          s = corpus.parse('schumann_clara/opus17', 3)
          # s.show()
          is1 = s.parts[0].flat.getElementsByClass('Instrument')
          self.assertEqual(len(is1), 1)
          # self.assertIn('Violin', is1[0].classes)
          is2 = s.parts[1].flat.getElementsByClass('Instrument')
          self.assertEqual(len(is2), 1)
          # self.assertIn('Violoncello', is1[0].classes)
          is3 = s.parts[2].flat.getElementsByClass('Instrument')
          self.assertEqual(len(is3), 1)
          # self.assertIn('Piano', is1[0].classes)
  
      def testConversionMidiBasic(self):
          dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive'
          fp = dirLib / 'test01.mid'
  
          # a simple file created in athenacl
  
          unused_s = parseFile(fp)
          unused_s = parse(fp)
  
          c = subConverters.ConverterMidi()
          c.parseFile(fp)
  
          # try low level string data passing
          with fp.open('rb') as f:
              data = f.read()
  
          c.parseData(data)
  
          # try module-level; function
          parseData(data)
          parse(data)
  
      def testConversionMidiNotes(self):
          from music21 import meter, key, chord, note
  
          fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test01.mid'
          # a simple file created in athenacl
          # for fn in ['test01.mid', 'test02.mid', 'test03.mid', 'test04.mid']:
          s = parseFile(fp)
          # s.show()
          self.assertEqual(len(s.flat.getElementsByClass(note.Note)), 18)
  
          # has chords and notes
          fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test05.mid'
          s = parseFile(fp)
          # s.show()
          # environLocal.printDebug(['\n' + 'opening fp', fp])
  
          self.assertEqual(len(s.flat.getElementsByClass(note.Note)), 2)
          self.assertEqual(len(s.flat.getElementsByClass(chord.Chord)), 4)
  
          self.assertEqual(len(s.flat.getElementsByClass(meter.TimeSignature)), 0)
          self.assertEqual(len(s.flat.getElementsByClass(key.KeySignature)), 0)
  
          # this sample has eight note triplets
          fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test06.mid'
          s = parseFile(fp)
          # s.show()
  
          # environLocal.printDebug(['\n' + 'opening fp', fp])
  
          # s.show()
          from fractions import Fraction as F
          dList = [n.quarterLength for n in s.flat.notesAndRests[:30]]
          match = [0.5, 0.5, 1.0, 0.5, 0.5, 0.5, 0.5, 1.0, 0.5, 0.5,
                   0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
                   0.5, 0.5, 0.5, 0.5, F(1, 3), F(1, 3), F(1, 3), 0.5, 0.5, 1.0]
          self.assertEqual(dList, match)
  
          self.assertEqual(len(s.flat.getElementsByClass('TimeSignature')), 1)
          self.assertEqual(len(s.flat.getElementsByClass('KeySignature')), 1)
  
          # this sample has sixteenth note triplets
          # TODO much work is still needed on getting timing right
          # this produces numerous errors in makeMeasure partitioning
          fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test07.mid'
          # environLocal.printDebug(['\n' + 'opening fp', fp])
          s = parseFile(fp)
          # s.show('t')
          self.assertEqual(len(s.flat.getElementsByClass('TimeSignature')), 1)
          self.assertEqual(len(s.flat.getElementsByClass('KeySignature')), 1)
  
          # this sample has dynamic changes in key signature
          fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test08.mid'
          # environLocal.printDebug(['\n' + 'opening fp', fp])
          s = parseFile(fp)
          # s.show('t')
          self.assertEqual(len(s.flat.getElementsByClass('TimeSignature')), 1)
          found = s.flat.getElementsByClass('KeySignature')
          self.assertEqual(len(found), 3)
          # test the right keys
          self.assertEqual(found[0].sharps, -3)
          self.assertEqual(found[1].sharps, 3)
          self.assertEqual(found[2].sharps, -1)
  
      def testConversionMXRepeats(self):
          from music21 import bar
          from music21.musicxml import testPrimitive
  
          mxString = testPrimitive.simpleRepeat45a
          s = parse(mxString)
  
          part = s.parts[0]
          measures = part.getElementsByClass('Measure')
          self.assertEqual(measures[0].leftBarline, None)
          self.assertEqual(measures[0].rightBarline.type, 'final')
  
          self.assertEqual(measures[1].leftBarline, None)
          self.assertEqual(measures[1].rightBarline.type, 'final')
  
          mxString = testPrimitive.repeatMultipleTimes45c
          s = parse(mxString)
  
          self.assertEqual(len(s.flat.getElementsByClass(bar.Barline)), 4)
          part = s.parts[0]
          measures = part.getElementsByClass('Measure')
  
          # s.show()
  
      def testConversionABCOpus(self):
  
          from music21.abcFormat import testFiles
          from music21 import corpus
  
          s = parse(testFiles.theAleWifesDaughter)
          # get a Stream object, not an opus
          self.assertIsInstance(s, stream.Score)
          self.assertNotIsInstance(s, stream.Opus)
          self.assertEqual(len(s.flat.notesAndRests), 66)
  
          # a small essen collection
          op = corpus.parse('essenFolksong/teste')
          # get a Stream object, not an opus
          # self.assertIsInstance(op, stream.Score)
          self.assertIsInstance(op, stream.Opus)
          self.assertEqual([len(s.flat.notesAndRests) for s in op],
                           [33, 51, 59, 33, 29, 174, 67, 88])
          # op.show()
  
          # get one work from the opus
          s = corpus.parse('essenFolksong/teste', number=6)
          self.assertIsInstance(s, stream.Score)
          self.assertNotIsInstance(s, stream.Opus)
          self.assertEqual(s.metadata.title, 'Moli hua')
  
          # s.show()
  
      def testConversionABCWorkFromOpus(self):
          # test giving a work number at loading
          from music21 import corpus
          s = corpus.parse('essenFolksong/han1', number=6)
          self.assertIsInstance(s, stream.Score)
          # noinspection SpellCheckingInspection
          self.assertEqual(s.metadata.title, 'Yi gan hongqi kongzhong piao')
          # make sure that beams are being made
          self.assertEqual(
              str(s.parts[0].flat.notesAndRests[4].beams),
              '<music21.beam.Beams <music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>>')
          # s.show()
  
      def testConversionMusedata(self):
          fp = common.getSourceFilePath() / 'musedata' / 'testPrimitive' / 'test01'
          s = parse(fp)
          self.assertEqual(len(s.parts), 5)
          # s.show()
  
      def testMixedArchiveHandling(self):
          '''
          Test getting data out of musedata or musicxml zip files.
          '''
          fp = common.getSourceFilePath() / 'musicxml' / 'testMxl.mxl'
          af = ArchiveManager(fp)
          # for now, only support zip
          self.assertEqual(af.archiveType, 'zip')
          self.assertTrue(af.isArchive())
          # if this is a musicxml file, there will only be single file; we
          # can call getData to get this
          post = af.getData()
          self.assertEqual(post[:38], '<?xml version="1.0" encoding="UTF-8"?>')
          self.assertEqual(af.getNames(), ['musicXML.xml', 'META-INF/', 'META-INF/container.xml'])
  
          # # test from a file that ends in zip
          # # note: this is a stage1 file!
          # fp = common.getSourceFilePath() / 'musedata' / 'testZip.zip'
          # af = ArchiveManager(fp)
          # # for now, only support zip
          # self.assertEqual(af.archiveType, 'zip')
          # self.assertTrue(af.isArchive())
          # self.assertEqual(af.getNames(), ['01/', '01/04', '01/02', '01/03', '01/01'] )
          #
          # # returns a list of strings
          # self.assertEqual(af.getData(dataFormat='musedata')[0][:30],
          #                  '378\n1080  1\nBach Gesells\nchaft')
  
          # mdw = musedataModule.MuseDataWork()
          # # can add a list of strings from getData
          # mdw.addString(af.getData(dataFormat='musedata'))
          # self.assertEqual(len(mdw.files), 4)
          #
          # mdpList = mdw.getParts()
          # self.assertEqual(len(mdpList), 4)
  
          # try to load parse the zip file
          # s = parse(fp)
  
          # test loading a directory
          fp = common.getSourceFilePath() / 'musedata' / 'testPrimitive' / 'test01'
          cmd = subConverters.ConverterMuseData()
          cmd.parseFile(fp)
  
      def testMEIvsMX(self):
          '''
          Ensure Converter.parseData() distinguishes between a string with MEI data and a string with
          MusicXML data. The "subConverter" module is mocked out because we don't actually need to
          test the conversion process in this unit test.
          '''
          # These strings aren't valid documents, but they are enough to pass the detection we're
          # testing in parseData(). But it does mean we'll be testing in a strange way.
          meiString = '<?xml version="1.0" encoding="UTF-8"?><mei><note/></mei>'
          # mxlString = ('<?xml version="1.0" encoding="UTF-8"?>' +
          #                '<score-partwise><note/></score-partwise>')
  
          # The "mei" module raises an MeiElementError with "meiString," so as long as that's raised,
          # we know that parseData() chose correctly.
          from music21.mei.base import MeiElementError
          testConv = Converter()
          self.assertRaises(MeiElementError, testConv.parseData, meiString)
  
          # TODO: another test -- score-partwise is good enough for new converter.
          # The ConverterMusicXML raises a SubConverterException with "mxlString," so as long as
          # that's raised, we know that parseData()... well at least that it didn't choose MEI.
          # from music21.converter.subConverters import SubConverterException
          # testConv = Converter()
          # self.assertRaises(SubConverterException, testConv.parseData, mxlString)
  
      def testParseMidiQuantize(self):
          '''
          Checks quantization when parsing a stream. Here everything snaps to the 8th note.
          '''
          from music21 import omr
          from music21.common import numberTools
          midiFp = omr.correctors.pathName + os.sep + 'k525short.mid'
          midiStream = parse(midiFp, forceSource=True, storePickle=False, quarterLengthDivisors=[2])
          # midiStream.show()
          for n in midiStream.recurse(classFilter='Note'):
              self.assertTrue(numberTools.almostEquals(n.quarterLength % 0.5, 0.0))
  
      def testParseMidiNoQuantize(self):
          '''
          Checks that quantization is not performed if quantizePost=False.
          Source MIDI file contains only: 3 16th notes, 2 32nd notes.
          '''
          fp = common.getSourceFilePath() / 'midi' / 'testPrimitive' / 'test15.mid'
  
          # Don't forceSource: test that pickles contemplate quantization keywords
          streamFpQuantized = parse(fp)
          self.assertNotIn(0.875, streamFpQuantized.flat._uniqueOffsetsAndEndTimes())
  
          streamFpNotQuantized = parse(fp, quantizePost=False)
          self.assertIn(0.875, streamFpNotQuantized.flat._uniqueOffsetsAndEndTimes())
  
          streamFpCustomQuantized = parse(fp, quarterLengthDivisors=[2])
          self.assertNotIn(0.75, streamFpCustomQuantized.flat._uniqueOffsetsAndEndTimes())
  
          # Also check raw data: https://github.com/cuthbertLab/music21/issues/546
          with fp.open('rb') as f:
              data = f.read()
          streamDataNotQuantized = parse(data, quantizePost=False)
          self.assertIn(0.875, streamDataNotQuantized.flat._uniqueOffsetsAndEndTimes())
  
          # Remove pickles so that failures are possible in future
          pf1 = PickleFilter(fp)
          pf1.removePickle()
          pf2 = PickleFilter(fp, quantizePost=False)
          pf2.removePickle()
          pf3 = PickleFilter(fp, quarterLengthDivisors=[2])
          pf3.removePickle()
  
      def testIncorrectNotCached(self):
          '''
          Here is a filename with an incorrect extension (.txt for .rnText).  Make sure that
          it is not cached the second time...
          '''
          fp = common.getSourceFilePath() / 'converter' / 'incorrectExtension.txt'
          pf = PickleFilter(fp)
          pf.removePickle()
  
          with self.assertRaises(ConverterFileException):
              parse(fp)
  
          c = parse(fp, format='romantext')
          self.assertEqual(len(c.recurse().getElementsByClass('Harmony')), 1)
  
      def testConverterFromPath(self):
          fp = common.getSourceFilePath() / 'corpus' / 'bach' / 'bwv66.6.mxl'
          s = parse(fp)
          self.assertIn('Stream', s.classes)
  
          fp = common.getSourceFilePath() / 'corpus' / 'bach' / 'bwv66.6'
  
          with self.assertRaises(FileNotFoundError):
              parse(fp)
  
      def testParseURL(self):
          urlBase = 'http://kern.ccarh.org/cgi-bin/ksdata?l=users/craig/classical/'
          url = urlBase + 'chopin/prelude&file=prelude28-20.krn&format=kern'
  
          e = environment.Environment()
          e['autoDownload'] = 'allow'
          s = parseURL(url)
          self.assertEqual(len(s.parts), 2)
  
  
  # ------------------------------------------------------------------------------
  # define presented order in documentation
  _DOC_ORDER = [parse, parseFile, parseData, parseURL, freeze, thaw, freezeStr, thawStr,
                Converter, registerSubconverter, unregisterSubconverter]
  
  
  if __name__ == '__main__':
      # sys.arg test options will be used in mainTest()
      import music21
      music21.mainTest(Test)  # , runTest='testConverterFromPath')
  
  
  