Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ __pycache__/
.mypy_cache/**
**/.mypy_cache/**

# we are a library, don't lock the requirements
uv.lock

# PyCharm
# Keep some to help new users...
.idea/codeStyles
Expand Down
4 changes: 0 additions & 4 deletions documentation/testDocumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,6 @@ def main(runOne: str|bool = False):
totalFailures = 0

timeStart = time.time()
unused_dtr = doctest.DocTestRunner(doctest.OutputChecker(),
verbose=False,
optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE)

for mt in getDocumentationFiles(runOne):
# if 'examples' in mt.module:
# continue
Expand Down
4 changes: 2 additions & 2 deletions music21/analysis/windowed.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def process(self,

# -----------------------------------------------------------------------------

class TestMockProcessor:
class MockObjectProcessor:

def process(self, subStream):
'''
Expand Down Expand Up @@ -380,7 +380,7 @@ def testWindowing(self):
'''
Test that windows are doing what they are supposed to do
'''
p = TestMockProcessor()
p = MockObjectProcessor()

from music21 import note
s1 = stream.Stream()
Expand Down
4 changes: 2 additions & 2 deletions music21/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ class AppendSpanners(StrEnum):
AppendSpanners.NONE means do not append the related spanners at all (i.e. only append
the object).
* new in v9.
* New in v9.
'''
NORMAL = 'normal'
RELATED_ONLY = 'related_only'
Expand All @@ -262,7 +262,7 @@ class OrnamentDelay(StrEnum):
OrnamentDelay.NO_DELAY means there is no delay (this is equivalent to setting delay to 0.0)
OrnamentDelay.DEFAULT_DELAY means the delay is half the duration of the ornamented note.
* new in v9.
* New in v9.
'''
NO_DELAY = 'noDelay'
DEFAULT_DELAY = 'defaultDelay'
Expand Down
27 changes: 13 additions & 14 deletions music21/common/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,21 +141,20 @@ def findFormat(fmt):

>>> common.findFormat('wpd')
(None, None)


These don't work but should eventually:

# >>> common.findFormat('png')
# ('musicxml.png', '.png')

# >>> common.findFormat('ipython')
# ('ipython', '.png')
# >>> common.findFormat('ipython.png')
# ('ipython', '.png')

# >>> common.findFormat('musicxml.png')
# ('musicxml.png', '.png')
'''
# These don't work but should eventually:
#
# # >>> common.findFormat('png')
# # ('musicxml.png', '.png')
#
# # >>> common.findFormat('ipython')
# # ('ipython', '.png')
# # >>> common.findFormat('ipython.png')
# # ('ipython', '.png')
#
# # >>> common.findFormat('musicxml.png')
# # ('musicxml.png', '.png')

from music21 import converter
c = converter.Converter()
fileFormat = c.regularizeFormat(fmt)
Expand Down
58 changes: 43 additions & 15 deletions music21/common/parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,24 @@
from __future__ import annotations

__all__ = [
'cpus',
'runParallel',
'runNonParallel',
'cpus',
'safeToParallize',
]

import multiprocessing
import os
import unittest


def runParallel(iterable, parallelFunction, *,
updateFunction=None, updateMultiply=3,
unpackIterable=False, updateSendsIterable=False):
def runParallel(iterable,
parallelFunction,
*,
updateFunction=None,
updateMultiply=3,
unpackIterable=False,
updateSendsIterable=False):
'''
runs parallelFunction over iterable in parallel, optionally calling updateFunction after
each common.cpus * updateMultiply calls.
Expand Down Expand Up @@ -103,9 +109,7 @@ def runParallel(iterable, parallelFunction, *,
>>> outputs
[99, 11, 123]
'''
numCpus = cpus()

if numCpus == 1 or multiprocessing.current_process().daemon:
if not safeToParallize():
return runNonParallel(iterable, parallelFunction,
updateFunction=updateFunction,
updateMultiply=updateMultiply,
Expand Down Expand Up @@ -135,14 +139,15 @@ def callUpdate(ii):
else:
thisResult = resultsList[thisPosition]

if updateSendsIterable is False:
if not updateSendsIterable:
updateFunction(thisPosition, iterLength, thisResult)
else:
updateFunction(thisPosition, iterLength, thisResult, iterable[thisPosition])

callUpdate(0)
from joblib import Parallel, delayed # type: ignore

numCpus = cpus()
with Parallel(n_jobs=numCpus) as para:
delayFunction = delayed(parallelFunction)
while totalRun < iterLength:
Expand All @@ -161,9 +166,13 @@ def callUpdate(ii):
return resultsList


def runNonParallel(iterable, parallelFunction, *,
updateFunction=None, updateMultiply=3,
unpackIterable=False, updateSendsIterable=False):
def runNonParallel(iterable,
parallelFunction,
*,
updateFunction=None,
updateMultiply=3,
unpackIterable=False,
updateSendsIterable=False):
'''
This is intended to be a perfect drop in replacement for runParallel, except that
it runs on one core only, and not in parallel.
Expand All @@ -190,7 +199,7 @@ def callUpdate(ii):
else:
thisResult = resultsList[thisPosition]

if updateSendsIterable is False:
if not updateSendsIterable:
updateFunction(thisPosition, iterLength, thisResult)
else:
updateFunction(thisPosition, iterLength, thisResult, iterable[thisPosition])
Expand Down Expand Up @@ -219,6 +228,22 @@ def cpus():
else:
return cpuCount

def safeToParallize() -> bool:
'''
Check to see if it is safe or even useful to start a parallel process.

Will return False if we are in a multiprocessing child process or if
there is only one CPU or if pytest's x-dist worker flag is in the environment.

* New in v10
'''
return (
cpus() > 1
and not multiprocessing.parent_process()
and 'PYTEST_XDIST_WORKER' not in os.environ
)


# Not shown to work.
# def pickleCopy(obj):
# '''
Expand Down Expand Up @@ -252,12 +277,15 @@ def x_figure_out_segfault_testMultiprocess(self):
from music21.common.parallel import _countN, _countUnpacked
output = runParallel(files, _countN)
self.assertEqual(output, [165, 50, 131])
runParallel(files, _countN,
runParallel(files,
_countN,
updateFunction=self._customUpdate1)
runParallel(files, _countN,
runParallel(files,
_countN,
updateFunction=self._customUpdate2,
updateSendsIterable=True)
passed = runParallel(list(enumerate(files)), _countUnpacked,
passed = runParallel(list(enumerate(files)),
_countUnpacked,
unpackIterable=True)
self.assertEqual(len(passed), 3)
self.assertNotIn(False, passed)
Expand Down
41 changes: 27 additions & 14 deletions music21/converter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
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')
>>> #_DOCS_SHOW s = converter.parse('D:/myDocs/schubert.krn')
>>> s = converter.parse(humdrum.testFiles.schubert) #_DOCS_HIDE
>>> s
<music21.stream.Score ...>
Expand Down Expand Up @@ -426,7 +426,7 @@ def registerSubConverter(newSubConverter: type[subConverters.SubConverter]) -> N
... x, scf[x]
('abc', <class 'music21.converter.subConverters.ConverterABC'>)
...
('sonix', <class 'music21.ConverterSonix'>)
('sonix', <class '...ConverterSonix'>)
...

See `converter.qmConverter` for an example of an extended subConverter.
Expand All @@ -444,14 +444,19 @@ def unregisterSubConverter(
'''
Remove a SubConverter from the list of registered subConverters.

(Note that the list is a shared list across all Converters currently --
that has long been considered a feature, but with multiprocessing, this
could change in the future.)

>>> converter.resetSubConverters() #_DOCS_HIDE
>>> mxlConverter = converter.subConverters.ConverterMusicXML

>>> c = converter.Converter()
>>> mxlConverter in c.subConvertersList()
True
>>> converter.unregisterSubConverter(mxlConverter)
>>> mxlConverter in c.subConvertersList()
>>> #_DOCS_SHOW converter.unregisterSubConverter(mxlConverter)
>>> #_DOCS_SHOW mxlConverter in c.subConvertersList()
>>> False #_DOCS_HIDE -- this breaks on parallel runs
False

If there is no such subConverter registered, and it is not a default subConverter,
Expand All @@ -460,18 +465,25 @@ def unregisterSubConverter(
>>> 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
music21.converter.ConverterException: Could not remove <class '...ConverterSonix'> from
registered subConverters

The special command "all" removes everything including the default converters:

>>> converter.unregisterSubConverter('all')
>>> c.subConvertersList()
>>> #_DOCS_SHOW converter.unregisterSubConverter('all')
>>> #_DOCS_SHOW c.subConvertersList()
>>> [] #_DOCS_HIDE
[]

>>> converter.resetSubConverters() #_DOCS_HIDE
Housekeeping: let's reset our subconverters and check things are okay again.

>>> converter.resetSubConverters()
>>> c = converter.Converter()
>>> mxlConverter in c.subConvertersList()
True
'''
if removeSubConverter == 'all':
_registeredSubConverters.clear()
Expand Down Expand Up @@ -594,7 +606,7 @@ def getFormatFromFileExtension(self, fp):
else:
useFormat = common.findFormatFile(fp)
if useFormat is None:
raise ConverterFileException(f'cannot find a format extensions for: {fp}')
raise ConverterFileException(f'cannot find format from file extensions for: {fp}')
return useFormat

# noinspection PyShadowingBuiltins
Expand Down Expand Up @@ -861,7 +873,7 @@ def subConvertersList(
>>> ConverterSonix in c.subConvertersList()
True

Newly registered subConveters appear first, so they will be used instead
Newly registered subConverters appear first, so they will be used instead
of any default subConverters that work on the same format or extension.

>>> class BadMusicXMLConverter(converter.subConverters.SubConverter):
Expand All @@ -872,7 +884,7 @@ def subConvertersList(

>>> converter.registerSubConverter(BadMusicXMLConverter)
>>> c.subConvertersList()
[<class 'music21.BadMusicXMLConverter'>,
[<class '...BadMusicXMLConverter'>,
...
<class 'music21.converter.subConverters.ConverterMusicXML'>,
...]
Expand All @@ -896,6 +908,8 @@ def subConvertersList(
>>> #_DOCS_SHOW s = corpus.parse('beach/prayer_of_a_tired_child')
>>> s.id
'empty'
>>> len(s.parts)
0
>>> s = corpus.parse('beach/prayer_of_a_tired_child', forceSource=True)
>>> len(s.parts)
6
Expand Down Expand Up @@ -1512,7 +1526,6 @@ def freezeStr(streamObj, fmt=None):
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>
Expand All @@ -1530,7 +1543,6 @@ def freezeStr(streamObj, fmt=None):
{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)
Expand Down Expand Up @@ -1570,7 +1582,8 @@ def testMusicXMLConversion(self):
from music21.musicxml import testFiles
for mxString in testFiles.ALL:
a = subConverters.ConverterMusicXML()
a.parseData(mxString)
a.parseData(mxString.strip())
break


class TestExternal(unittest.TestCase):
Expand Down
11 changes: 6 additions & 5 deletions music21/features/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import unittest

from music21 import common
from music21.common.parallel import safeToParallize
from music21.common.types import StreamType
from music21 import converter
from music21 import corpus
Expand Down Expand Up @@ -931,7 +932,7 @@ def process(self):
Process all Data with all FeatureExtractors.
Processed data is stored internally as numerous Feature objects.
'''
if self.runParallel:
if self.runParallel and safeToParallize():
return self._processParallel()
else:
return self._processNonParallel()
Expand All @@ -947,10 +948,10 @@ def _processParallel(self):

# print('about to run parallel')
outputData = common.runParallel([(di, self.failFast) for di in self.dataInstances],
_dataSetParallelSubprocess,
updateFunction=shouldUpdate,
updateMultiply=1,
unpackIterable=True
_dataSetParallelSubprocess,
updateFunction=shouldUpdate,
updateMultiply=1,
unpackIterable=True
)
featureData, errors, classValues, ids = zip(*outputData)
errors = common.flattenList(errors)
Expand Down
2 changes: 1 addition & 1 deletion music21/figuredBass/harmony.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class FiguredBass(Harmony):
>>> fb.pitches
()

* new in v9.3
* New in v9.3
'''
def __init__(self,
figureString: str = '',
Expand Down
2 changes: 2 additions & 0 deletions music21/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2399,6 +2399,8 @@ def convertValue(uniqueName: str, value: t.Any) -> ValueType:
>>> metadata.Metadata.convertValue('dateCreated',
... metadata.DateBetween(['1938', '1939']))
<music21.metadata.primitives.DateBetween 1938/--/-- to 1939/--/-->

* Added in v10 -- newly exposed as a public function (was private)
'''
valueType: type[ValueType]|None = properties.UNIQUE_NAME_TO_VALUE_TYPE.get(
uniqueName, None
Expand Down
Loading