# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
# Name:         documentation/library/documenters.py
# Purpose:      music21 documentation helper classes for proper formatting
#
# Authors:      Josiah Wolf Oberholtzer
#               Christopher Ariza
#               Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2013-17 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# -----------------------------------------------------------------------------
from __future__ import annotations

import builtins
import inspect
import re
import types
import unittest

from music21 import common
from music21.exceptions21 import Music21Exception


class Documenter:
    '''
    Base class for documenting classes.
    '''
    def run(self):
        pass

    def __repr__(self):
        return ''

    # PRIVATE PROPERTIES #

    @property
    def _packageSystemPath(self):
        return '.'.join(
            (
                'docbuild',
                'documenters',
                self.__class__.__name__,
            )
        )

    # PUBLIC PROPERTIES #

    @property
    def rstEditingWarningFormat(self):
        result = [
            '.. WARNING: DO NOT EDIT THIS FILE:',
            '   AUTOMATICALLY GENERATED.',
            '   PLEASE EDIT THE .py FILE DIRECTLY.',
            '',
        ]
        return result

    # PUBLIC METHODS #

    @staticmethod
    def makeHeading(text, heading_level):
        if not text or not isinstance(text, str):
            raise Music21Exception('make heading requires text')
        heading_characters = ['=', '-', '^', '"']
        result = [
            text,
            heading_characters[heading_level - 1] * len(text),
            '',
        ]
        return result

    @staticmethod
    def makeRubric(text):
        return [
            f'.. rubric:: {text}',
            '',
        ]


class ObjectDocumenter(Documenter):
    '''
    Base class for object documenting subclasses. such as ClassDocumenter
    '''

    _DOC_ATTR: dict[str, str] = {
        'referent':
            ''''
            The object being documented.
            ''',
    }
    # INITIALIZER #

    sphinxCrossReferenceRole = ''

    def __init__(self, referent):
        self.referent = referent

    # PUBLIC PROPERTIES #

    @property
    def referentPackageSystemPath(self):
        return ''

    @property
    def rstAutodocDirectiveFormat(self) -> list[str]:
        return []

    @property
    def rstCrossReferenceString(self):
        return f':{self.sphinxCrossReferenceRole}:`~{self.referentPackageSystemPath}`'


class FunctionDocumenter(ObjectDocumenter):
    '''
    A documenter for one function:

    >>> function = common.opFrac
    >>> documenter = FunctionDocumenter(function)
    >>> documenter
    <docbuild.documenters.FunctionDocumenter: music21.common.numberTools.opFrac>

    >>> documenter.rstCrossReferenceString
    ':func:`~music21.common.numberTools.opFrac`'

    >>> for line in documenter.rstAutodocDirectiveFormat:
    ...     line
    '.. autofunction:: music21.common.numberTools.opFrac'
    ''

    Call the documenter to generate its ReStructuredText format:

    >>> restructuredText = documenter.run()
    >>> restructuredText
    ['.. autofunction:: music21.common.numberTools.opFrac', '']
    '''

    sphinxCrossReferenceRole = 'func'

    # INITIALIZER #

    def __init__(self, referent=None):
        # noinspection PyTypeChecker
        if not isinstance(referent, types.FunctionType):
            raise Music21Exception('referent must be a function')
        super().__init__(referent)

    # SPECIAL METHODS #

    def run(self):
        result = []
        result.extend(self.rstAutodocDirectiveFormat)
        return result

    def __repr__(self):
        return f'<{self._packageSystemPath}: {self.referentPackageSystemPath}>'

    # PUBLIC PROPERTIES #

    @property
    def referentPackageSystemPath(self):
        '''
        >>> function = common.opFrac
        >>> documenter = FunctionDocumenter(function)
        >>> documenter.referentPackageSystemPath
        'music21.common.numberTools.opFrac'
        '''
        path = '.'.join(
            (
                self.referent.__module__,
                self.referent.__name__,
            )
        )
        return path.replace('.__init__', '')

    @property
    def rstAutodocDirectiveFormat(self) -> list[str]:
        '''
        >>> function = common.opFrac
        >>> documenter = FunctionDocumenter(function)
        >>> documenter.rstAutodocDirectiveFormat
        ['.. autofunction:: music21.common.numberTools.opFrac', '']
        '''
        result = []
        referentPackageSystemPath = self.referentPackageSystemPath.replace(
            '.__init__', '')
        result.append(f'.. autofunction:: {referentPackageSystemPath}')
        result.append('')
        return result


class MemberDocumenter(ObjectDocumenter):
    '''
    Abstract base class for documenting class members such as Methods and Attributes and Properties
    '''
    _DOC_ATTR: dict[str, str] = {
        'memberName': 'the short name of the member, for instance "mode"',
        'referent': '''the attribute or method itself, such as
                       <function KeySignature.mode at ...>''',
        'definingClass': '''the class the referent belongs to, such as
                            <class 'music21.key.KeySignature'>''',
    }

    # INITIALIZER #

    def __init__(self, referent, memberName: str, definingClass: type):
        if not isinstance(definingClass, type):
            raise Music21Exception(f'referent must be a class, not {referent}')
        super().__init__(referent)
        self.memberName = memberName
        self.definingClass = definingClass

    # SPECIAL METHODS #

    def run(self):
        result = []
        result.extend(self.rstAutodocDirectiveFormat)
        return result

    def __repr__(self):
        referentPath = '.'.join(
            (
                self.definingClass.__module__,
                self.definingClass.__name__,
                self.memberName,
            )
        )
        return f'<{self._packageSystemPath}: {referentPath}>'

    # PUBLIC PROPERTIES #

    @property
    def referentPackageSystemPath(self):
        path = '.'.join(
            (
                self.definingClass.__module__,
                self.definingClass.__name__,
                self.memberName,
            )
        )
        return path.replace('.__init__', '')

    @property
    def rstAutodocDirectiveFormat(self):
        return []


class MethodDocumenter(MemberDocumenter):
    '''
    A documenter for class methods:

    >>> from music21 import key
    >>> method = key.KeySignature.transpose
    >>> documenter = MethodDocumenter(
    ...     method, 'transpose', key.KeySignature)
    >>> documenter
    <docbuild.documenters.MethodDocumenter: music21.key.KeySignature.transpose>

    >>> documenter.rstCrossReferenceString
    ':meth:`~music21.key.KeySignature.transpose`'

    >>> for line in documenter.rstAutodocDirectiveFormat:
    ...     line
    '.. automethod:: music21.key.KeySignature.transpose'
    ''
    '''
    sphinxCrossReferenceRole = 'meth'

    # PUBLIC PROPERTIES #

    @property
    def rstAutodocDirectiveFormat(self):
        result = []
        indent = ''
        if getattr(self.referent, '_isDeprecated', False):
            indent = '    '
            result.append('.. cssclass:: strike')
            result.append('')
        result.append(f'{indent}.. automethod:: {self.referentPackageSystemPath}')
        result.append('')
        return result



class AttributeDocumenter(MemberDocumenter):
    '''
    A documenter for class attributes, both read/write and read-only:

    >>> from music21 import key
    >>> attribute = key.KeySignature.sharps
    >>> documenter = AttributeDocumenter(
    ...     attribute, 'sharps', key.KeySignature)
    >>> documenter
    <docbuild.documenters.AttributeDocumenter: music21.key.KeySignature.sharps>

    >>> documenter.rstCrossReferenceString
    ':attr:`~music21.key.KeySignature.sharps`'

    >>> for line in documenter.rstAutodocDirectiveFormat:
    ...     line
    '.. autoattribute:: music21.key.KeySignature.sharps'
    ''
    '''
    sphinxCrossReferenceRole = 'attr'

    # PUBLIC PROPERTIES #

    @property
    def rstAutodocDirectiveFormat(self):
        result = []
        result.append(f'.. autoattribute:: {self.referentPackageSystemPath}')
        result.append('')
        return result


class ClassDocumenter(ObjectDocumenter):
    '''
    A documenter for one class:

    >>> from music21 import articulations
    >>> klass = articulations.Caesura
    >>> documenter = ClassDocumenter(klass)
    >>> documenter
    <docbuild.documenters.ClassDocumenter: music21.articulations.Caesura>
    >>> documenter.referent
    <class 'music21.articulations.Caesura'>

    >>> documenter.rstCrossReferenceString
    ':class:`~music21.articulations.Caesura`'

    >>> for line in documenter.rstAutodocDirectiveFormat:
    ...     line
    ...
    '.. autoclass:: music21.articulations.Caesura'
    ''

    Generate the ReST lines by calling `run` on the documenter:

    >>> rstList = documenter.run()
    >>> for l in rstList:
    ...     if len(l) != 0:
    ...         print(l)
    Caesura
    -------
    .. autoclass:: music21.articulations.Caesura
    .. rubric:: :class:`~music21.articulations.Caesura` bases
    - :class:`~music21.articulations.Articulation`
    - :class:`~music21.base.Music21Object`
    - :class:`~music21.prebase.ProtoM21Object`
    .. rubric:: :class:`~music21.articulations.Caesura` read-only properties
    Read-only properties inherited from :class:`~music21.articulations.Articulation`:
    .. hlist::
       :columns: 3
       - :attr:`~music21.articulations.Articulation.name`
    Read-only properties inherited from :class:`~music21.base.Music21Object`:
    .. hlist::
       :columns: 3
       - :attr:`~music21.base.Music21Object.beat`
       - :attr:`~music21.base.Music21Object.beatDuration`
                ...
       - :attr:`~music21.base.Music21Object.measureNumber`
    Read-only properties inherited from :class:`~music21.prebase.ProtoM21Object`:
    .. hlist::
       :columns: 3
       - :attr:`~music21.prebase.ProtoM21Object.classSet`
       - :attr:`~music21.prebase.ProtoM21Object.classes`
    .. rubric:: :class:`~music21.articulations.Caesura` read/write properties
    Read/write properties inherited from :class:`~music21.articulations.Articulation`:
    .. hlist::
       :columns: 3
       - :attr:`~music21.articulations.Articulation.volumeShift`
    Read/write properties inherited from :class:`~music21.base.Music21Object`:
    .. hlist::
       :columns: 3
       - :attr:`~music21.base.Music21Object.activeSite`
       - :attr:`~music21.base.Music21Object.derivation`
                ...
       - :attr:`~music21.base.Music21Object.style`
    .. rubric:: :class:`~music21.articulations.Caesura` methods
    Methods inherited from :class:`~music21.base.Music21Object`:
    .. hlist::
       :columns: 3
       - :meth:`~music21.base.Music21Object.clearCache`
       - :meth:`~music21.base.Music21Object.containerHierarchy`
       - :meth:`~music21.base.Music21Object.contextSites`
                ...
       - :meth:`~music21.base.Music21Object.splitAtQuarterLength`
       - :meth:`~music21.base.Music21Object.splitByQuarterLengths`
       - :meth:`~music21.base.Music21Object.write`
    Methods inherited from :class:`~music21.prebase.ProtoM21Object`:
    .. hlist::
       :columns: 3
       - :meth:`~music21.prebase.ProtoM21Object.isClassOrSubclass`
    .. rubric:: :class:`~music21.articulations.Caesura` instance variables
    Instance variables inherited from :class:`~music21.base.Music21Object`:
    .. hlist::
       :columns: 3
       - :attr:`~music21.base.Music21Object.classSortOrder`
       - :attr:`~music21.base.Music21Object.groups`
       - :attr:`~music21.base.Music21Object.isStream`
       - :attr:`~music21.base.Music21Object.sites`
    '''

    # CLASS VARIABLES #
    sphinxCrossReferenceRole = 'class'
    _identityMap = {}

    # INITIALIZER #

    def __init__(self, referent=None):
        if referent is None or not isinstance(referent, type):
            raise Music21Exception('Need to pass in a class an instantiation time')
        super().__init__(referent)


        self._baseClasses = None
        self._baseClassDocumenters = None
        self._docAttr = None
        self._docOrder = None
        self._inheritedDocAttrMapping = None

        self._methods = []
        self._inheritedMethodsMapping = {}

        # Read/Write
        self._readwriteProperties = []
        self._inheritedReadwritePropertiesMapping = {}

        # Read-only
        self._readonlyProperties = []
        self._inheritedReadonlyPropertiesMapping = {}

        # Special (dunder) methods to document, if overridden in music21
        self._allowedSpecialMethods = ['__eq__', '__getitem__']

        self.findAttributes()

        if self.referent not in self._identityMap:
            self._identityMap[self.referent] = self


    def findAttributes(self):
        '''
        find all attributes in self.referent and set classes appropriately.
        '''

        attrs: list[inspect.Attribute] = inspect.classify_class_attrs(self.referent)
        for attr in attrs:
            self.findOneAttribute(attr)

        self.sortMemberLists()

    def sortMemberLists(self):
        '''
        sort all the member lists by their member names.
        '''
        keyLambda = lambda x: x.memberName
        self._methods.sort(key=keyLambda)
        self._readonlyProperties.sort(key=keyLambda)
        self._readwriteProperties.sort(key=keyLambda)
        for documenters in self._inheritedMethodsMapping.values():
            documenters.sort(key=keyLambda)
        for documenters in self._inheritedReadonlyPropertiesMapping.values():
            documenters.sort(key=keyLambda)
        for documenters in self._inheritedReadwritePropertiesMapping.values():
            documenters.sort(key=keyLambda)


    def findOneAttribute(self, attr: inspect.Attribute):
        # Ignore definitions derived directly from builtins (object, list, etc.)
        if attr.defining_class in builtins.__dict__.values():
            return
        # Ignore private members ('_') and special members ('__')
        elif attr.name.startswith('_') and attr.name not in self._allowedSpecialMethods:
            return

        definingClass = attr.defining_class
        if attr.kind in ('class method', 'method', 'static method'):
            documenterClass = MethodDocumenter
            localMemberList = self._methods
            inheritedMembersMapping = self._inheritedMethodsMapping
        elif attr.kind in ('property',) and attr.object.fset is not None:
            documenterClass = AttributeDocumenter
            localMemberList = self._readwriteProperties
            inheritedMembersMapping = self._inheritedReadwritePropertiesMapping
        elif attr.kind in ('property',) and attr.object.fset is None:
            documenterClass = AttributeDocumenter
            localMemberList = self._readonlyProperties
            inheritedMembersMapping = self._inheritedReadonlyPropertiesMapping
        else:  # do not support write-only properties
            return

        documenter = documenterClass(
            attr.object,
            attr.name,
            definingClass,
        )
        if definingClass is self.referent:
            localMemberList.append(documenter)
        else:
            definingClassDocumenter = type(self).fromIdentityMap(
                definingClass
            )
            if definingClassDocumenter not in inheritedMembersMapping:
                inheritedMembersMapping[definingClassDocumenter] = []
            inheritedMembersMapping[definingClassDocumenter].append(documenter)

    # SPECIAL METHODS #

    def run(self):
        result = []
        result.extend(self.makeHeading(self.referent.__name__, 2))
        result.extend(self.rstAutodocDirectiveFormat)
        result.extend(self.rstBasesFormat)
        result.extend(self.rstReadonlyPropertiesFormat)
        result.extend(self.rstReadwritePropertiesFormat)
        result.extend(self.rstMethodsFormat)
        result.extend(self.rstDocAttrFormat)
        return result

    def __hash__(self):
        return hash((type(self), self.referent))

    def __repr__(self):
        referentPath = '.'.join(
            (
                self.referent.__module__,
                self.referent.__name__,
            )
        )
        return f'<{self._packageSystemPath}: {referentPath}>'

    # PRIVATE METHODS #

    def _formatInheritedMembersMapping(self, mapping, banner):
        result = []
        if not mapping:
            return result
        for classDocumenter in self.baseClassDocumenters:
            if classDocumenter not in mapping:
                continue
            result.append(banner.format(
                classDocumenter.rstCrossReferenceString))
            result.append('')
            memberDocumenters = mapping[classDocumenter]
            result.append('.. hlist::')
            result.append('   :columns: 3')
            result.append('')
            for memberDocumenter in memberDocumenters:
                result.append(f'   - {memberDocumenter.rstCrossReferenceString}')
            result.append('')
        return result

    # PUBLIC METHODS #

    @classmethod
    def fromIdentityMap(cls, referent):
        if referent in cls._identityMap:
            return cls._identityMap[referent]
        return cls(referent)

    # PUBLIC PROPERTIES #

    @property
    def baseClasses(self):
        '''
        >>> from music21 import articulations
        >>> klass = articulations.Caesura
        >>> documenter = ClassDocumenter(klass)
        >>> documenter.baseClasses
        (<class 'music21.articulations.Articulation'>,
         <class 'music21.base.Music21Object'>,
         <class 'music21.prebase.ProtoM21Object'>)
        '''
        if self._baseClasses is None:
            self._baseClasses = tuple(
                cls for cls in inspect.getmro(self.referent)[1:]
                if cls.__module__.startswith('music21'))

        return self._baseClasses

    @property
    def baseClassDocumenters(self):
        if self._baseClassDocumenters is None:
            self._baseClassDocumenters = tuple(
                type(self).fromIdentityMap(cls) for cls in self.baseClasses
            )
        return self._baseClassDocumenters

    @property
    def docAttr(self):
        '''
        The music21 _DOC_ATTR definition for a documented class:

        >>> from music21 import stream
        >>> klass = stream.Stream
        >>> documenter = ClassDocumenter(klass)
        >>> for key in sorted(list(documenter.docAttr.keys())):
        ...     key
        ...
        'autoSort'
        'definesExplicitPageBreaks'
        'definesExplicitSystemBreaks'
        'isFlat'
        'isSorted'
        'recursionType'
        '''
        if self._docAttr is None:
            attrs = inspect.classify_class_attrs(self.referent)
            docAttrAttr = [attr for attr in attrs if attr.name == '_DOC_ATTR']
            if docAttrAttr:
                docAttrAttr = docAttrAttr[0]
                if docAttrAttr.defining_class is self.referent:
                    self._docAttr = self.referent._DOC_ATTR

        return self._docAttr

    @property
    def docOrder(self):
        '''
        The music21 _DOC_ORDER definition for a documented class:

        >>> from music21 import dynamics
        >>> klass = dynamics.Dynamic
        >>> documenter = ClassDocumenter(klass)
        >>> for name in documenter.docOrder:
        ...     name
        'longName'
        'englishName'
        '''
        self._docOrder = getattr(self.referent, '_DOC_ORDER', [])
        return self._docOrder


    @property
    def inheritedDocAttrMapping(self):
        '''
        A mapping of parent class documenters and doc-attr attribute dicts
        for a documented class:

        >>> from music21 import stream
        >>> klass = stream.Measure
        >>> documenter = ClassDocumenter(klass)
        >>> mapping = documenter.inheritedDocAttrMapping
        >>> sortBy = lambda x: x.referentPackageSystemPath
        >>> for classDocumenter in sorted(mapping, key=sortBy):
        ...     classDocumenter
        ...
        <docbuild.documenters.ClassDocumenter: music21.base.Music21Object>
        <docbuild.documenters.ClassDocumenter: music21.prebase.ProtoM21Object>
        <docbuild.documenters.ClassDocumenter: music21.stream.base.Stream>
        '''
        # if one of the _DOC_ATTRs is exactly the same as the previous base class,
        # only show it once.
        # TODO: do this on an attribute by attribute basis...
        seenBaseClassDocAttrs = []

        if self._inheritedDocAttrMapping is None:
            inheritedDocAttr = {}
            for baseClass in reversed(self.baseClasses):
                baseClassDocAttr = getattr(baseClass, '_DOC_ATTR', None)
                if (baseClassDocAttr is not None
                        and baseClassDocAttr not in seenBaseClassDocAttrs):
                    baseClassDocumenter = type(self).fromIdentityMap(baseClass)
                    inheritedDocAttr[baseClassDocumenter] = baseClassDocAttr
                    seenBaseClassDocAttrs.append(baseClassDocAttr)
            self._inheritedDocAttrMapping = inheritedDocAttr

        return self._inheritedDocAttrMapping

    @property
    def inheritedReadonlyPropertiesMapping(self):
        '''
        A mapping of parent class documenters and inherited read-only
        properties for a documented class:

        >>> from music21 import stream
        >>> klass = stream.Measure
        >>> documenter = ClassDocumenter(klass)
        >>> mapping = documenter.inheritedReadonlyPropertiesMapping
        >>> sortBy = lambda x: x.referentPackageSystemPath
        >>> for classDocumenter in sorted(mapping, key=sortBy):
        ...     print(f'{classDocumenter.referentPackageSystemPath}:')
        ...     for attributeDocumenter in mapping[classDocumenter][:10]:
        ...         print(f'- {attributeDocumenter.referentPackageSystemPath}')
        ...
        music21.base.Music21Object:
        - music21.base.Music21Object.hasEditorialInformation
        - music21.base.Music21Object.hasStyleInformation
        - music21.base.Music21Object.measureNumber
        music21.prebase.ProtoM21Object:
        - music21.prebase.ProtoM21Object.classSet
        - music21.prebase.ProtoM21Object.classes
        music21.stream.base.Stream:
        - music21.stream.base.Stream.beat
        - music21.stream.base.Stream.beatDuration
        - music21.stream.base.Stream.beatStr
        - music21.stream.base.Stream.beatStrength
        - music21.stream.base.Stream.flat
        - music21.stream.base.Stream.highestOffset
        - music21.stream.base.Stream.highestTime
        - music21.stream.base.Stream.isGapless
        - music21.stream.base.Stream.lowestOffset
        - music21.stream.base.Stream.notes
        music21.stream.core.StreamCore:
        - music21.stream.core.StreamCore.spannerBundle
        '''
        return self._inheritedReadonlyPropertiesMapping

    @property
    def inheritedMethodsMapping(self):
        '''
        A mapping of parent class documenters and inherited attributes for a
        documented class:

        >>> from music21 import stream
        >>> klass = stream.Measure
        >>> documenter = ClassDocumenter(klass)
        >>> mapping = documenter.inheritedMethodsMapping
        >>> sortBy = lambda x: x.referentPackageSystemPath
        >>> for classDocumenter in sorted(mapping, key=sortBy):
        ...     print(f'{classDocumenter.referentPackageSystemPath}:')
        ...     for attributeDocumenter in mapping[classDocumenter][:5]:
        ...         print(f'- {attributeDocumenter.referentPackageSystemPath}')
        ...
        music21.base.Music21Object:
        - music21.base.Music21Object.clearCache
        - music21.base.Music21Object.containerHierarchy
        - music21.base.Music21Object.contextSites
        - music21.base.Music21Object.getAllContextsByClass
        - music21.base.Music21Object.getContextByClass
        music21.prebase.ProtoM21Object:
        - music21.prebase.ProtoM21Object.isClassOrSubclass
        music21.stream.base.Stream:
        - music21.stream.base.Stream.__getitem__
        - music21.stream.base.Stream.activateVariants
        - music21.stream.base.Stream.addGroupForElements
        - music21.stream.base.Stream.allPlayingWhileSounding
        - music21.stream.base.Stream.analyze
        music21.stream.core.StreamCore:
        - music21.stream.core.StreamCore.asTimespans
        - music21.stream.core.StreamCore.asTree
        - music21.stream.core.StreamCore.coreAppend
        - music21.stream.core.StreamCore.coreCopyAsDerivation
        - music21.stream.core.StreamCore.coreElementsChanged
        '''
        return self._inheritedMethodsMapping

    @property
    def inheritedReadwritePropertiesMapping(self):
        '''
        A mapping of parent class documenters and inherited read/write
        properties for a documented class:

        >>> from music21 import stream
        >>> klass = stream.Measure
        >>> documenter = ClassDocumenter(klass)
        >>> mapping = documenter.inheritedReadwritePropertiesMapping
        >>> sortBy = lambda x: x.referentPackageSystemPath
        >>> for classDocumenter in sorted(mapping, key=sortBy):
        ...     print('{0}:'.format(classDocumenter.referentPackageSystemPath))
        ...     for attributeDocumenter in mapping[classDocumenter][:10]:
        ...         print('- {0}'.format(attributeDocumenter.referentPackageSystemPath))
        ...
        music21.base.Music21Object:
        - music21.base.Music21Object.activeSite
        - music21.base.Music21Object.derivation
        - music21.base.Music21Object.editorial
        - music21.base.Music21Object.id
        ...
        - music21.base.Music21Object.style
        music21.stream.base.Stream:
        - music21.stream.base.Stream.atSoundingPitch
        - music21.stream.base.Stream.clef
        ...
        - music21.stream.base.Stream.seconds
        - music21.stream.base.Stream.staffLines
        - music21.stream.base.Stream.timeSignature

        '''
        return self._inheritedReadwritePropertiesMapping

    @property
    def methods(self):
        '''
        The method documenters for a documented class:

        >>> from music21 import stream
        >>> klass = stream.Stream
        >>> documenter = ClassDocumenter(klass)
        >>> for method in documenter.methods[:10]:
        ...     method
        ...
        <docbuild.documenters.MethodDocumenter:
            music21.stream.base.Stream.__getitem__>
        <docbuild.documenters.MethodDocumenter:
            music21.stream.base.Stream.activateVariants>
        <docbuild.documenters.MethodDocumenter:
            music21.stream.base.Stream.addGroupForElements>
        <docbuild.documenters.MethodDocumenter:
            music21.stream.base.Stream.allPlayingWhileSounding>
        <docbuild.documenters.MethodDocumenter:
            music21.stream.base.Stream.analyze>
        <docbuild.documenters.MethodDocumenter:
            music21.stream.base.Stream.append>
        <docbuild.documenters.MethodDocumenter:
            music21.stream.base.Stream.attachIntervalsBetweenStreams>
        <docbuild.documenters.MethodDocumenter:
            music21.stream.base.Stream.attachMelodicIntervals>
        <docbuild.documenters.MethodDocumenter:
            music21.stream.base.Stream.augmentOrDiminish>
        <docbuild.documenters.MethodDocumenter:
            music21.stream.base.Stream.beatAndMeasureFromOffset>
        '''
        return self._methods

    @property
    def readonlyProperties(self):
        '''
        The read-only property documenters for a documented class:

        >>> from music21 import stream
        >>> klass = stream.Stream
        >>> documenter = ClassDocumenter(klass)
        >>> for attr in documenter.readonlyProperties:
        ...     attr
        ...
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.beat>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.beatDuration>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.beatStr>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.beatStrength>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.flat>
        ...
        '''
        return self._readonlyProperties

    @property
    def readwriteProperties(self):
        '''
        The read/write property documenters for a documented class:

        >>> from music21 import stream
        >>> klass = stream.Stream
        >>> documenter = ClassDocumenter(klass)
        >>> for prop in documenter.readwriteProperties:
        ...     prop
        ...
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.atSoundingPitch>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.clef>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.duration>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.elements>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.finalBarline>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.keySignature>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.metadata>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.seconds>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.staffLines>
        <docbuild.documenters.AttributeDocumenter: music21.stream.base.Stream.timeSignature>
        '''
        return self._readwriteProperties

    @property
    def referentPackageSystemPath(self):
        path = '.'.join(
            (
                self.referent.__module__,
                self.referent.__name__,
            )
        )
        return path.replace('.__init__', '')

    @property
    def rstAutodocDirectiveFormat(self):
        result = []
        referentPackageSystemPath = self.referentPackageSystemPath.replace(
            '.__init__', ''
        )
        result.append(f'.. autoclass:: {referentPackageSystemPath}')
        result.append('')
        return result

    @property
    def rstBasesFormat(self):
        '''
        The ReST format for the bases from which the documented class
        inherits:

        >>> from music21 import note
        >>> klass = note.NotRest
        >>> documenter = ClassDocumenter(klass)
        >>> for line in documenter.rstBasesFormat:
        ...     line
        ...
        '.. rubric:: :class:`~music21.note.NotRest` bases'
        ''
        '- :class:`~music21.note.GeneralNote`'
        '- :class:`~music21.base.Music21Object`'
        '- :class:`~music21.prebase.ProtoM21Object`'
        ''

        '''
        result = []
        if self.baseClasses:
            banner = f'{self.rstCrossReferenceString} bases'
            result.extend(self.makeRubric(banner))
            # result.extend(self.makeHeading(banner, 3))
            for class_documenter in self.baseClassDocumenters:
                result.append(f'- {class_documenter.rstCrossReferenceString}')
            result.append('')
        return result

    @property
    def rstDocAttrFormat(self):
        result = []
        if self.docAttr:
            for attrName, attrDescription in sorted(self.docAttr.items()):
                lastRef = self.referentPackageSystemPath.rsplit('.', maxsplit=1)[-1]
                path = f'{lastRef}.{attrName}'
                directive = f'.. attribute:: {path}'
                result.extend((directive, ''))
                for line in attrDescription.split('\n'):
                    result.append(f'\t{line.strip()}')
                result.append('')
        result.extend(self.rstInheritedDocAttrFormat)
        if result:
            banner = f'{self.rstCrossReferenceString} instance variables'
            result = self.makeRubric(banner) + result
            # result = self.makeHeading(banner, 3) + result
        return result

    @property
    def rstInheritedDocAttrFormat(self):
        result = []
        for baseDocumenter in self.baseClassDocumenters:
            if baseDocumenter in self.inheritedDocAttrMapping:
                attrNames = sorted(list(self.inheritedDocAttrMapping[baseDocumenter].keys()))
                if not attrNames:
                    continue
                crossStr = baseDocumenter.rstCrossReferenceString
                banner = f'Instance variables inherited from {crossStr}:'
                result.extend((banner, ''))
                result.append('.. hlist::')
                result.append('   :columns: 3')
                result.append('')
                formatString = '   - :attr:`~{0}.{1}`'
                for attrName in attrNames:
                    result.append(
                        formatString.format(
                            baseDocumenter.referentPackageSystemPath,
                            attrName,
                        )
                    )
                result.append('')
        return result

    @property
    def rstInheritedMethodsFormat(self):
        r'''
        The ReST format for inherited methods:

        >>> from music21 import note
        >>> klass = note.Note
        >>> documenter = ClassDocumenter(klass)
        >>> for line in documenter.rstInheritedMethodsFormat:
        ...     line
        ...
        'Methods inherited from :class:`~music21.note.NotRest`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :meth:`~music21.note.NotRest.getInstrument`'
        '   - :meth:`~music21.note.NotRest.hasVolumeInformation`'
        ''
        'Methods inherited from :class:`~music21.note.GeneralNote`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :meth:`~music21.note.GeneralNote.addLyric`'
        '   - :meth:`~music21.note.GeneralNote.augmentOrDiminish`'
        '   - :meth:`~music21.note.GeneralNote.getGrace`'
        ...
        ''
        'Methods inherited from :class:`~music21.base.Music21Object`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :meth:`~music21.base.Music21Object.clearCache`'
        '   - :meth:`~music21.base.Music21Object.containerHierarchy`'
        '   - :meth:`~music21.base.Music21Object.contextSites`'
        ...
        ''

        '''
        mapping = self.inheritedMethodsMapping
        banner = 'Methods inherited from {0}:'
        return self._formatInheritedMembersMapping(mapping, banner)


    @property
    def rstInheritedReadonlyPropertiesFormat(self):
        r'''
        The ReST format for inherited methods:

        >>> from music21 import note
        >>> klass = note.Note
        >>> documenter = ClassDocumenter(klass)
        >>> for line in documenter.rstInheritedReadonlyPropertiesFormat:
        ...     line
        'Read-only properties inherited from :class:`~music21.base.Music21Object`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :attr:`~music21.base.Music21Object.beat`'
        '   - :attr:`~music21.base.Music21Object.beatDuration`'
        '   - :attr:`~music21.base.Music21Object.beatStr`'
        '   - :attr:`~music21.base.Music21Object.beatStrength`'
        '   - :attr:`~music21.base.Music21Object.hasEditorialInformation`'
        '   - :attr:`~music21.base.Music21Object.hasStyleInformation`'
        '   - :attr:`~music21.base.Music21Object.measureNumber`'
        ''
        'Read-only properties inherited from :class:`~music21.prebase.ProtoM21Object`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :attr:`~music21.prebase.ProtoM21Object.classSet`'
        '   - :attr:`~music21.prebase.ProtoM21Object.classes`'
        ''
        '''
        mapping = self.inheritedReadonlyPropertiesMapping
        banner = 'Read-only properties inherited from {0}:'
        return self._formatInheritedMembersMapping(mapping, banner)

    @property
    def rstInheritedReadwritePropertiesFormat(self):
        '''
        The ReST format for inherited methods:

        >>> from music21 import note
        >>> klass = note.Unpitched
        >>> documenter = ClassDocumenter(klass)
        >>> for line in documenter.rstInheritedReadwritePropertiesFormat:
        ...     line
        'Read/write properties inherited from :class:`~music21.note.NotRest`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :attr:`~music21.note.NotRest.notehead`'
        '   - :attr:`~music21.note.NotRest.noteheadFill`'
        '   - :attr:`~music21.note.NotRest.noteheadParenthesis`'
        '   - :attr:`~music21.note.NotRest.stemDirection`'
        '   - :attr:`~music21.note.NotRest.volume`'
        ''
        'Read/write properties inherited from :class:`~music21.note.GeneralNote`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :attr:`~music21.note.GeneralNote.lyric`'
        '   - :attr:`~music21.note.GeneralNote.pitches`'
        '   - :attr:`~music21.note.GeneralNote.tie`'
        ''
        'Read/write properties inherited from :class:`~music21.base.Music21Object`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :attr:`~music21.base.Music21Object.activeSite`'
        '   - :attr:`~music21.base.Music21Object.derivation`'
        '   - :attr:`~music21.base.Music21Object.duration`'
        '   - :attr:`~music21.base.Music21Object.editorial`'
        '   - :attr:`~music21.base.Music21Object.id`'
        '   - :attr:`~music21.base.Music21Object.offset`'
        '   - :attr:`~music21.base.Music21Object.priority`'
        '   - :attr:`~music21.base.Music21Object.quarterLength`'
        '   - :attr:`~music21.base.Music21Object.seconds`'
        '   - :attr:`~music21.base.Music21Object.style`'
        ''
        '''
        mapping = self.inheritedReadwritePropertiesMapping
        banner = 'Read/write properties inherited from {0}:'
        return self._formatInheritedMembersMapping(mapping, banner)

    @property
    def rstMethodsFormat(self):
        r'''
        The ReST format for the documented class's methods:

        >>> from music21 import scale
        >>> klass = scale.MajorScale
        >>> classDocumenter = ClassDocumenter(klass)
        >>> for line in classDocumenter.rstMethodsFormat:
        ...     line
        '.. rubric:: :class:`~music21.scale.MajorScale` methods'
        ''
        'Methods inherited from :class:`~music21.scale.DiatonicScale`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :meth:`~music21.scale.DiatonicScale.getDominant`'
        ...
        '   - :meth:`~music21.scale.DiatonicScale.getTonic`'
        ''
        'Methods inherited from :class:`~music21.scale.ConcreteScale`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :meth:`~music21.scale.ConcreteScale.__eq__`'
        '   - :meth:`~music21.scale.ConcreteScale.derive`'
        '   - :meth:`~music21.scale.ConcreteScale.deriveAll`'
        ...
        '   - :meth:`~music21.scale.ConcreteScale.write`'
        ''
        'Methods inherited from :class:`~music21.scale.Scale`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :meth:`~music21.scale.Scale.extractPitchList`'
        ''
        'Methods inherited from :class:`~music21.base.Music21Object`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :meth:`~music21.base.Music21Object.clearCache`'
        ...
        '   - :meth:`~music21.base.Music21Object.splitByQuarterLengths`'
        ''
        'Methods inherited from :class:`~music21.prebase.ProtoM21Object`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :meth:`~music21.prebase.ProtoM21Object.isClassOrSubclass`'
        ''

        '''
        result = []
        if self.methods:
            for documenter in self.methods:
                result.extend(documenter.run())
            if result[-1] != '':
                result.append('')
        result.extend(self.rstInheritedMethodsFormat)
        if result:
            banner = f'{self.rstCrossReferenceString} methods'
            # result = self.makeHeading(banner, 3) + result
            result = self.makeRubric(banner) + result
        return result

    @property
    def rstReadonlyPropertiesFormat(self):
        r'''
        The ReST format for the documented class's read-only properties:

        >>> from music21 import note
        >>> klass = note.Note
        >>> classDocumenter = ClassDocumenter(klass)
        >>> for line in classDocumenter.rstReadonlyPropertiesFormat:
        ...     line
        '.. rubric:: :class:`~music21.note.Note` read-only properties'
        ''
        '.. autoattribute:: music21.note.Note.fullName'
        ''
        'Read-only properties inherited from :class:`~music21.base.Music21Object`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :attr:`~music21.base.Music21Object.beat`'
        '   - :attr:`~music21.base.Music21Object.beatDuration`'
        '   - :attr:`~music21.base.Music21Object.beatStr`'
        '   - :attr:`~music21.base.Music21Object.beatStrength`'
        '   - :attr:`~music21.base.Music21Object.hasEditorialInformation`'
        '   - :attr:`~music21.base.Music21Object.hasStyleInformation`'
        '   - :attr:`~music21.base.Music21Object.measureNumber`'
        ''
        'Read-only properties inherited from :class:`~music21.prebase.ProtoM21Object`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :attr:`~music21.prebase.ProtoM21Object.classSet`'
        '   - :attr:`~music21.prebase.ProtoM21Object.classes`'
        ''

        '''
        result = []
        if self.readonlyProperties:
            for documenter in self.readonlyProperties:
                result.extend(documenter.run())
            if result[-1] != '':
                result.append('')
        result.extend(self.rstInheritedReadonlyPropertiesFormat)
        if result:
            banner = f'{self.rstCrossReferenceString} read-only properties'
            result = self.makeRubric(banner) + result
        return result

    @property
    def rstReadwritePropertiesFormat(self):
        '''
        The ReST format for the documented class's read-only properties:

        >>> from music21 import scale
        >>> klass = scale.MajorScale
        >>> classDocumenter = ClassDocumenter(klass)
        >>> for line in classDocumenter.rstReadwritePropertiesFormat:
        ...     line
        '.. rubric:: :class:`~music21.scale.MajorScale` read/write properties'
        ''
        'Read/write properties inherited from :class:`~music21.scale.ConcreteScale`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :attr:`~music21.scale.ConcreteScale.abstract`'
        ''
        'Read/write properties inherited from :class:`~music21.base.Music21Object`:'
        ''
        '.. hlist::'
        '   :columns: 3'
        ''
        '   - :attr:`~music21.base.Music21Object.activeSite`'
        '   - :attr:`~music21.base.Music21Object.derivation`'
        '   - :attr:`~music21.base.Music21Object.duration`'
        '   - :attr:`~music21.base.Music21Object.editorial`'
        '   - :attr:`~music21.base.Music21Object.id`'
        '   - :attr:`~music21.base.Music21Object.offset`'
        '   - :attr:`~music21.base.Music21Object.priority`'
        '   - :attr:`~music21.base.Music21Object.quarterLength`'
        '   - :attr:`~music21.base.Music21Object.seconds`'
        '   - :attr:`~music21.base.Music21Object.style`'
        ''
        '''
        result = []
        if self.readwriteProperties:
            for documenter in self.readwriteProperties:
                result.extend(documenter.run())
            if result[-1] != '':
                result.append('')
        result.extend(self.rstInheritedReadwritePropertiesFormat)
        if result:
            banner = f'{self.rstCrossReferenceString} read/write properties'
            result = self.makeRubric(banner) + result
        return result


class ModuleDocumenter(ObjectDocumenter):
    '''
    A documenter for one module:

    >>> from music21 import serial
    >>> documenter = ModuleDocumenter(serial)
    >>> documenter
    <docbuild.documenters.ModuleDocumenter: music21.serial>

    >>> for reference, referent in sorted(list(
    ...     documenter.namesMapping.items())):
    ...     print(f'{reference} {referent}')
    ...
    HistoricalTwelveToneRow <...ClassDocumenter: music21.serial.HistoricalTwelveToneRow>
    ToneRow <...ClassDocumenter: music21.serial.ToneRow>
    TwelveToneMatrix <...ClassDocumenter: music21.serial.TwelveToneMatrix>
    TwelveToneRow <...ClassDocumenter: music21.serial.TwelveToneRow>
    getHistoricalRowByName <...FunctionDocumenter: music21.serial.getHistoricalRowByName>
    pcToToneRow <...FunctionDocumenter: music21.serial.pcToToneRow>
    rowToMatrix <...FunctionDocumenter: music21.serial.rowToMatrix>

    >>> documenter.rstCrossReferenceString
    ':mod:`~music21.serial`'

    >>> for line in documenter.rstAutodocDirectiveFormat:
    ...     line
    ...
    '.. automodule:: music21.serial'
    ''

    Call the documenter to generate the ReStructuredText format:

    >>> restructuredText = documenter.run()

    '''

    # CLASS VARIABLES #
    sphinxCrossReferenceRole = 'mod'

    _ignored_classes = frozenset(
        (
            BaseException,
            unittest.TestCase,
        )
    )

    # INITIALIZER #

    def __init__(self, referent):
        if not isinstance(referent, types.ModuleType):
            raise Music21Exception('referent must be a module')
        super().__init__(referent)
        namesMapping = self._examineModule()
        self._namesMapping = namesMapping
        docOrder = self.referent.__dict__.get('_DOC_ORDER')
        if docOrder is not None and not common.isListLike(docOrder):
            print('Doc order for ', self.referent, ' has problems')
        self._memberOrder = tuple(docOrder or ())

    # SPECIAL METHODS #

    def run(self):
        result = []
        result.extend(self.rstPageReferenceFormat)
        referentPackageSystemPath = self.referentPackageSystemPath.replace(
            '.__init__', ''
        )
        result.extend(self.makeHeading(referentPackageSystemPath, 1))
        result.extend(self.rstEditingWarningFormat)
        result.extend(self.rstAutodocDirectiveFormat)
        for classDocumenter in self.classDocumenters:
            result.extend(classDocumenter.run())
        if self.functionDocumenters:
            result.extend(self.makeHeading('Functions', 2))
            for functionDocumenter in self.functionDocumenters:
                result.extend(functionDocumenter.run())
        return result

    def __repr__(self):
        return f'<{self._packageSystemPath}: {self.referentPackageSystemPath}>'

    # PRIVATE METHODS #

    def _examineModule(self):
        namesMapping = {}
        for name in dir(self.referent):
            if name.startswith('_'):
                continue
            named = getattr(self.referent, name)
            # noinspection PyTypeChecker
            if isinstance(named, type):
                if set(inspect.getmro(named)).intersection(
                    self._ignored_classes
                ):
                    continue
                if named.__module__ != self.referent.__name__:
                    continue
                namesMapping[name] = ClassDocumenter(named)
            elif (isinstance(named, types.FunctionType)
                    and not named.__name__ == '<lambda>'):
                if named.__module__ != self.referent.__name__:
                    continue
                namesMapping[name] = FunctionDocumenter(named)
        return namesMapping

    # PUBLIC PROPERTIES #

    @property
    def classDocumenters(self):
        '''
        Return a list of class documenters for classes housed in the
        module documenter's module object, taking into account any ordering
        via the `_DOC_ORDER` module-level variable:

        >>> from music21 import serial
        >>> module = serial
        >>> modDocumenter = ModuleDocumenter(module)
        >>> for classDocumenter in modDocumenter.classDocumenters:
        ...     print(classDocumenter.referentPackageSystemPath)
        ...
        music21.serial.ToneRow
        music21.serial.TwelveToneRow
        music21.serial.HistoricalTwelveToneRow
        music21.serial.TwelveToneMatrix
        '''
        result = []
        classDocumenters = {}
        for documenter in self.namesMapping.values():
            if isinstance(documenter, ClassDocumenter):
                classDocumenters[documenter.referent] = documenter
        for referent in self.memberOrder:
            if referent in classDocumenters:
                result.append(classDocumenters[referent])
                del classDocumenters[referent]
        for documenter in sorted(
                classDocumenters.values(),
                key=lambda x: x.referentPackageSystemPath):
            result.append(documenter)
        return result

    @property
    def functionDocumenters(self):
        '''
        Return a list of class documenters for classes housed in the
        module documenter's module object, taking into account any ordering
        via the `_DOC_ORDER` module-level variable:

        >>> from music21 import serial
        >>> module = serial
        >>> modDocumenter = ModuleDocumenter(module)
        >>> for functionDocumenter in modDocumenter.functionDocumenters:
        ...     print(functionDocumenter.referentPackageSystemPath)
        ...
        music21.serial.pcToToneRow
        music21.serial.rowToMatrix
        music21.serial.getHistoricalRowByName
        '''
        result = []
        functionDocumenters = {}
        for documenter in self.namesMapping.values():
            if isinstance(documenter, FunctionDocumenter):
                functionDocumenters[documenter.referent] = documenter
        for referent in self.memberOrder:
            if referent in functionDocumenters:
                result.append(functionDocumenters[referent])
                del functionDocumenters[referent]
        for documenter in sorted(
            functionDocumenters.values(),
            key=lambda x: x.referentPackageSystemPath
        ):
            result.append(documenter)
        return result

    @property
    def namesMapping(self):
        return self._namesMapping

    @property
    def memberOrder(self):
        return self._memberOrder

    @property
    def referentPackageSystemPath(self):
        if isinstance(self.referent.__name__, tuple):
            path = self.referent.__name__[0]
        else:
            path = self.referent.__name__
        return path.replace('.__init__', '')

    @property
    def rstAutodocDirectiveFormat(self):
        result = []
        result.append(f'.. automodule:: {self.referentPackageSystemPath}')
        result.append('')
        return result

    @property
    def rstPageReferenceFormat(self):
        result = []
        result.append(f'.. _{self.referenceName}:')
        result.append('')
        return result

    @property
    def referenceName(self):
        '''
        The short name of the module:

        >>> from music21 import serial
        >>> module = serial
        >>> documenter = ModuleDocumenter(module)
        >>> documenter.referenceName
        'moduleSerial'

        >>> from music21.stream import makeNotation
        >>> module = makeNotation
        >>> documenter = ModuleDocumenter(module)
        >>> documenter.referenceName
        'moduleStreamMakeNotation'

        '''
        referentPackageSystemPath = self.referentPackageSystemPath.replace(
            '.__init__', '')
        parts = referentPackageSystemPath.split('.')[1:]
        for i, part in enumerate(parts):
            if not part[0].isupper():
                parts[i] = part[0].upper() + part[1:]
        parts = ['module'] + parts
        return ''.join(parts)


class CorpusDocumenter(Documenter):
    '''
    A documenter for music21's corpus:

    >>> documenter = CorpusDocumenter()
    >>> restructuredText = documenter.run()
    '''

    # SPECIAL METHODS #

    def run(self):
        from music21 import corpus
        result = []
        result.extend(self.rstPageReferenceFormat)
        result.extend(self.makeHeading(self.headingText, 1))
        result.extend(self.rstEditingWarningFormat)
        result.extend(self.rstCorpusIntroductionFormat)
        # TODO: use... common.getCorpusContentDirs to make sure nothing is missed.
        for directoryInformation in corpus.corpora.CoreCorpus().getWorkReferences():
            result.extend(self.getRstComposerDictFormat(directoryInformation))
        return result

    def __repr__(self):
        return f'<{self._packageSystemPath}>'

    # PUBLIC PROPERTIES #

    @property
    def headingText(self):
        return 'List of Works Found in the `music21` Corpus'

    @property
    def rstCorpusIntroductionFormat(self):
        result = []
        result.append('The following list shows all files available in the ')
        result.append('`music21` corpus.')
        result.append('To load a work from the corpus, provide the file path ')
        result.append('stub provided.')
        result.append('')
        result.append('For example:')
        result.append('')
        result.append('::')
        result.append('')
        result.append('   >>> from music21 import corpus')
        result.append("   >>> s = corpus.parse('bach/bwv108.6.xml')")
        result.append('')
        return result

    @property
    def rstPageReferenceFormat(self):
        result = []
        result.append('.. _referenceCorpus:')
        result.append('')
        return result

    # PUBLIC METHODS #

    def getRstComposerDictFormat(self, directoryInformation):
        result = []
        result.extend(self.makeHeading(directoryInformation.directoryTitle, 2))
        result.extend(self.getRstComposerIntroductionFormat(directoryInformation))
        for corpusWork in directoryInformation.works.values():
            result.extend(self.getRstComposerWorksFormat(corpusWork))
        return result

    @staticmethod
    def getRstComposerIntroductionFormat(directoryInformation):
        name = directoryInformation.directoryTitle
        directory = directoryInformation.directoryName
        isComposer = directoryInformation.isComposer

        result = []
        result.append('To get all works ')
        if isComposer:
            result.append(f'composed by {name},')
        else:
            result.append(f'collected in {name},')
        result.append('use :meth:`~music21.corpus.getComposer` ')
        result.append('to get all file paths.')
        result.append('')
        result.append('For example:')
        result.append('')
        result.append('::')
        result.append('')
        result.append(f'    >>> paths = corpus.getComposer({directory!r})')
        result.append('')
        return result

    def getRstComposerWorksFormat(self, corpusWork):
        result = []
        isSingleWork = (len(corpusWork.files) == 1)
        workTitle = str(corpusWork.title)
        # worksAreVirtual = corpusWork.virtual
        # if worksAreVirtual:
        #     workTitle += ' (*virtual*)'
        if isSingleWork is False:
            result.append(workTitle)
            result.append('')
        procedure = self.getRstWorkFileDictFormat
        # if worksAreVirtual:
        #     procedure = self.getRstVirtualWorkFileDictFormat
        if isSingleWork is False:
            for corpusFile in corpusWork.files:
                result.extend(['- ' + procedure(corpusFile), ''])
        else:
            result.append(procedure(corpusWork.files[0]))
            result.append('')
        return result

    # def getRstVirtualWorkFileDictFormat(self, corpusFile):
    #     result = []
    #     result.append('- {0} *({1})*: `{2}`'.format(
    #         str(corpusFile.title),
    #         str(corpusFile.format),
    #         str(corpusFile.path),
    #         ))
    #     result.append('')
    #     result.append('  Source: {0}'.format(
    #         str(corpusFile.url)))
    #     result.append('')
    #     return result

    def getRstWorkFileDictFormat(self, corpusFile):
        corpusPathWithoutSlashes = re.sub(
            '\\\\',
            '/',
            corpusFile.path,
        )
        result = f'{corpusFile.title} *({corpusFile.format})*: `{corpusPathWithoutSlashes}`'
        return result


if __name__ == '__main__':
    import music21
    music21.mainTest('moduleRelative')
