# ------------------------------------------------------------------------------
# Name: common/pathTools.py
# Purpose: Utilities for paths
#
# Authors: Michael Scott Asato Cuthbert
# Christopher Ariza
#
# Copyright: Copyright © 2009-2015 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
from __future__ import annotations
__all__ = [
'getRootFilePath',
'getSourceFilePath',
'getMetadataCacheFilePath',
'getCorpusFilePath',
'getCorpusContentDirs',
'relativepath',
'cleanpath',
]
import typing as t
from typing import overload
import inspect
import os
import pathlib
import unittest
StrOrPath = t.TypeVar('StrOrPath', bound=str|pathlib.Path)
# ------------------------------------------------------------------------------
[docs]
def getSourceFilePath() -> pathlib.Path:
'''
Get the music21 directory that contains source files such as note.py, etc.
This is not the same as the
outermost package development directory.
'''
fpThis = pathlib.Path(inspect.getfile(getSourceFilePath)).resolve()
fpMusic21 = fpThis.parent.parent # common is two levels deep
# use stream as a test case
if 'stream' not in [x.name for x in fpMusic21.iterdir()]:
raise FileNotFoundError(
f'cannot find expected music21 directory: {fpMusic21}'
) # pragma: no cover
return fpMusic21
[docs]
def getCorpusFilePath() -> pathlib.Path:
r'''
Get the stored music21 directory that contains the corpus metadata cache.
>>> fp = common.getCorpusFilePath()
>>> fp.name == 'corpus' and fp.parent.name == 'music21'
True
'''
from music21 import corpus
coreCorpus = corpus.corpora.CoreCorpus()
if coreCorpus.manualCoreCorpusPath is None:
return getSourceFilePath() / 'corpus'
return pathlib.Path(coreCorpus.manualCoreCorpusPath)
[docs]
def getCorpusContentDirs() -> list[str]:
'''
Get all dirs that are found in the CoreCorpus that contain content;
that is, exclude dirs that have code or other resources.
>>> fp = common.getCorpusContentDirs()
>>> fp # this list will be fragile, depending on composition of dirs
['airdsAirs', 'bach', 'beach', 'beethoven', 'chopin',
'ciconia', 'corelli', 'cpebach',
'demos', 'essenFolksong', 'handel', 'haydn', 'johnson_j_r', 'joplin', 'josquin',
'leadSheet', 'liliuokalani', 'luca', 'lusitano',
'miscFolk', 'monteverdi', 'mozart',
'nottingham-dataset',
'oneills1850', 'palestrina',
'ryansMammoth', 'schoenberg', 'schubert', 'schumann_clara', 'schumann_robert',
'theoryExercises', 'trecento', 'verdi', 'weber', 'webern']
Make sure that all corpus data has a directoryInformation tag in
CoreCorpus.
>>> cc = corpus.corpora.CoreCorpus()
>>> failed = []
>>> di = [d.directoryName for d in cc.directoryInformation]
>>> for f in fp:
... if f not in di:
... failed.append(f)
>>> failed
[]
'''
directoryName = getCorpusFilePath()
result = []
# dirs to exclude; all files will be retained
excludedNames = (
'license.txt',
'_metadataCache',
'__pycache__',
)
filename: str
for filename in sorted(os.listdir(directoryName)):
if filename.endswith(('.py', '.pyc')):
continue
elif filename.startswith('.'):
continue
elif filename in excludedNames:
continue
result.append(filename)
return sorted(result)
[docs]
def getRootFilePath() -> pathlib.Path:
'''
Return the root directory for music21 -- outside the music21 namespace
which has directories such as "dist", "documentation", "music21"
>>> fp = common.getRootFilePath()
>>> #_DOCS_SHOW fp
PosixPath('/Users/florencePrice/git/music21')
'''
fpMusic21 = getSourceFilePath()
fpParent = fpMusic21.parent
# Do not assume will end in music21 -- people can put this anywhere they want
return fpParent
[docs]
def relativepath(path: StrOrPath, start: str|None = None) -> StrOrPath|str:
'''
A cross-platform wrapper for `os.path.relpath()`, which returns `path` if
under Windows, otherwise returns the relative path of `path`.
This avoids problems under Windows when the current working directory is
on a different drive letter from `path`.
'''
import platform
if platform == 'Windows':
return path
return os.path.relpath(path, start)
@overload
def cleanpath(path: pathlib.Path, *,
returnPathlib: t.Literal[None] = None) -> pathlib.Path:
...
@overload
def cleanpath(path: str, *,
returnPathlib: t.Literal[None] = None) -> str:
...
@overload
def cleanpath(path: str|pathlib.Path, *,
returnPathlib: t.Literal[True]) -> pathlib.Path:
...
@overload
def cleanpath(path: str|pathlib.Path, *,
returnPathlib: t.Literal[False]) -> str:
...
[docs]
def cleanpath(path: str|pathlib.Path, *,
returnPathlib: bool|None = None) -> str|pathlib.Path:
'''
Normalizes the path by expanding ~user on Unix, ${var} environmental vars
(is this a good idea?), expanding %name% on Windows, normalizing path names
(Windows turns backslashes to forward slashes), and finally if that file
is not an absolute path, turns it from a relative path to an absolute path.
v5 -- returnPathlib -- None (default) does not convert. False, returns a string,
True, returns a pathlib.Path.
'''
if isinstance(path, pathlib.Path):
path = str(path)
if returnPathlib is None:
returnPathlib = True
elif returnPathlib is None:
returnPathlib = False
path = os.path.expanduser(path)
path = os.path.normpath(path)
if not os.path.isabs(path):
path = os.path.abspath(path)
path = os.path.expandvars(path)
if not returnPathlib:
return path
else:
return pathlib.Path(path)
class Test(unittest.TestCase):
def testGetSourcePath(self):
fp = getSourceFilePath()
self.assertIsInstance(fp, pathlib.Path)
if __name__ == '__main__':
import music21
music21.mainTest(Test)