Skip to content

Commit 6cc266d

Browse files
authored
Merge pull request #1851 from cuthbertLab/pytest-compat
Pytest Compatibility
2 parents 59cf9ab + 96dbcde commit 6cc266d

File tree

17 files changed

+185
-68
lines changed

17 files changed

+185
-68
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ __pycache__/
66
.mypy_cache/**
77
**/.mypy_cache/**
88

9+
# we are a library, don't lock the requirements
10+
uv.lock
11+
912
# PyCharm
1013
# Keep some to help new users...
1114
.idea/codeStyles

documentation/testDocumentation.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,6 @@ def main(runOne: str|bool = False):
200200
totalFailures = 0
201201

202202
timeStart = time.time()
203-
unused_dtr = doctest.DocTestRunner(doctest.OutputChecker(),
204-
verbose=False,
205-
optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE)
206-
207203
for mt in getDocumentationFiles(runOne):
208204
# if 'examples' in mt.module:
209205
# continue

music21/analysis/windowed.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ def process(self,
345345

346346
# -----------------------------------------------------------------------------
347347

348-
class TestMockProcessor:
348+
class MockObjectProcessor:
349349

350350
def process(self, subStream):
351351
'''
@@ -380,7 +380,7 @@ def testWindowing(self):
380380
'''
381381
Test that windows are doing what they are supposed to do
382382
'''
383-
p = TestMockProcessor()
383+
p = MockObjectProcessor()
384384

385385
from music21 import note
386386
s1 = stream.Stream()

music21/common/enums.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ class AppendSpanners(StrEnum):
247247
AppendSpanners.NONE means do not append the related spanners at all (i.e. only append
248248
the object).
249249
250-
* new in v9.
250+
* New in v9.
251251
'''
252252
NORMAL = 'normal'
253253
RELATED_ONLY = 'related_only'
@@ -262,7 +262,7 @@ class OrnamentDelay(StrEnum):
262262
OrnamentDelay.NO_DELAY means there is no delay (this is equivalent to setting delay to 0.0)
263263
OrnamentDelay.DEFAULT_DELAY means the delay is half the duration of the ornamented note.
264264
265-
* new in v9.
265+
* New in v9.
266266
'''
267267
NO_DELAY = 'noDelay'
268268
DEFAULT_DELAY = 'defaultDelay'

music21/common/formats.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -141,21 +141,20 @@ def findFormat(fmt):
141141
142142
>>> common.findFormat('wpd')
143143
(None, None)
144-
145-
146-
These don't work but should eventually:
147-
148-
# >>> common.findFormat('png')
149-
# ('musicxml.png', '.png')
150-
151-
# >>> common.findFormat('ipython')
152-
# ('ipython', '.png')
153-
# >>> common.findFormat('ipython.png')
154-
# ('ipython', '.png')
155-
156-
# >>> common.findFormat('musicxml.png')
157-
# ('musicxml.png', '.png')
158144
'''
145+
# These don't work but should eventually:
146+
#
147+
# # >>> common.findFormat('png')
148+
# # ('musicxml.png', '.png')
149+
#
150+
# # >>> common.findFormat('ipython')
151+
# # ('ipython', '.png')
152+
# # >>> common.findFormat('ipython.png')
153+
# # ('ipython', '.png')
154+
#
155+
# # >>> common.findFormat('musicxml.png')
156+
# # ('musicxml.png', '.png')
157+
159158
from music21 import converter
160159
c = converter.Converter()
161160
fileFormat = c.regularizeFormat(fmt)

music21/common/parallel.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,24 @@
1010
from __future__ import annotations
1111

1212
__all__ = [
13+
'cpus',
1314
'runParallel',
1415
'runNonParallel',
15-
'cpus',
16+
'safeToParallize',
1617
]
1718

1819
import multiprocessing
20+
import os
1921
import unittest
2022

2123

22-
def runParallel(iterable, parallelFunction, *,
23-
updateFunction=None, updateMultiply=3,
24-
unpackIterable=False, updateSendsIterable=False):
24+
def runParallel(iterable,
25+
parallelFunction,
26+
*,
27+
updateFunction=None,
28+
updateMultiply=3,
29+
unpackIterable=False,
30+
updateSendsIterable=False):
2531
'''
2632
runs parallelFunction over iterable in parallel, optionally calling updateFunction after
2733
each common.cpus * updateMultiply calls.
@@ -103,9 +109,7 @@ def runParallel(iterable, parallelFunction, *,
103109
>>> outputs
104110
[99, 11, 123]
105111
'''
106-
numCpus = cpus()
107-
108-
if numCpus == 1 or multiprocessing.current_process().daemon:
112+
if not safeToParallize():
109113
return runNonParallel(iterable, parallelFunction,
110114
updateFunction=updateFunction,
111115
updateMultiply=updateMultiply,
@@ -135,14 +139,15 @@ def callUpdate(ii):
135139
else:
136140
thisResult = resultsList[thisPosition]
137141

138-
if updateSendsIterable is False:
142+
if not updateSendsIterable:
139143
updateFunction(thisPosition, iterLength, thisResult)
140144
else:
141145
updateFunction(thisPosition, iterLength, thisResult, iterable[thisPosition])
142146

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

150+
numCpus = cpus()
146151
with Parallel(n_jobs=numCpus) as para:
147152
delayFunction = delayed(parallelFunction)
148153
while totalRun < iterLength:
@@ -161,9 +166,13 @@ def callUpdate(ii):
161166
return resultsList
162167

163168

164-
def runNonParallel(iterable, parallelFunction, *,
165-
updateFunction=None, updateMultiply=3,
166-
unpackIterable=False, updateSendsIterable=False):
169+
def runNonParallel(iterable,
170+
parallelFunction,
171+
*,
172+
updateFunction=None,
173+
updateMultiply=3,
174+
unpackIterable=False,
175+
updateSendsIterable=False):
167176
'''
168177
This is intended to be a perfect drop in replacement for runParallel, except that
169178
it runs on one core only, and not in parallel.
@@ -190,7 +199,7 @@ def callUpdate(ii):
190199
else:
191200
thisResult = resultsList[thisPosition]
192201

193-
if updateSendsIterable is False:
202+
if not updateSendsIterable:
194203
updateFunction(thisPosition, iterLength, thisResult)
195204
else:
196205
updateFunction(thisPosition, iterLength, thisResult, iterable[thisPosition])
@@ -219,6 +228,22 @@ def cpus():
219228
else:
220229
return cpuCount
221230

231+
def safeToParallize() -> bool:
232+
'''
233+
Check to see if it is safe or even useful to start a parallel process.
234+
235+
Will return False if we are in a multiprocessing child process or if
236+
there is only one CPU or if pytest's x-dist worker flag is in the environment.
237+
238+
* New in v10
239+
'''
240+
return (
241+
cpus() > 1
242+
and not multiprocessing.parent_process()
243+
and 'PYTEST_XDIST_WORKER' not in os.environ
244+
)
245+
246+
222247
# Not shown to work.
223248
# def pickleCopy(obj):
224249
# '''
@@ -252,12 +277,15 @@ def x_figure_out_segfault_testMultiprocess(self):
252277
from music21.common.parallel import _countN, _countUnpacked
253278
output = runParallel(files, _countN)
254279
self.assertEqual(output, [165, 50, 131])
255-
runParallel(files, _countN,
280+
runParallel(files,
281+
_countN,
256282
updateFunction=self._customUpdate1)
257-
runParallel(files, _countN,
283+
runParallel(files,
284+
_countN,
258285
updateFunction=self._customUpdate2,
259286
updateSendsIterable=True)
260-
passed = runParallel(list(enumerate(files)), _countUnpacked,
287+
passed = runParallel(list(enumerate(files)),
288+
_countUnpacked,
261289
unpackIterable=True)
262290
self.assertEqual(len(passed), 3)
263291
self.assertNotIn(False, passed)

music21/converter/__init__.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
faster since we store a parsed version of each file as a "pickle" object in
2828
the temp folder on the disk.
2929
30-
>>> #_DOCS_SHOW s = converter.parse('d:/myDocs/schubert.krn')
30+
>>> #_DOCS_SHOW s = converter.parse('D:/myDocs/schubert.krn')
3131
>>> s = converter.parse(humdrum.testFiles.schubert) #_DOCS_HIDE
3232
>>> s
3333
<music21.stream.Score ...>
@@ -426,7 +426,7 @@ def registerSubConverter(newSubConverter: type[subConverters.SubConverter]) -> N
426426
... x, scf[x]
427427
('abc', <class 'music21.converter.subConverters.ConverterABC'>)
428428
...
429-
('sonix', <class 'music21.ConverterSonix'>)
429+
('sonix', <class '...ConverterSonix'>)
430430
...
431431
432432
See `converter.qmConverter` for an example of an extended subConverter.
@@ -444,14 +444,19 @@ def unregisterSubConverter(
444444
'''
445445
Remove a SubConverter from the list of registered subConverters.
446446
447+
(Note that the list is a shared list across all Converters currently --
448+
that has long been considered a feature, but with multiprocessing, this
449+
could change in the future.)
450+
447451
>>> converter.resetSubConverters() #_DOCS_HIDE
448452
>>> mxlConverter = converter.subConverters.ConverterMusicXML
449453
450454
>>> c = converter.Converter()
451455
>>> mxlConverter in c.subConvertersList()
452456
True
453-
>>> converter.unregisterSubConverter(mxlConverter)
454-
>>> mxlConverter in c.subConvertersList()
457+
>>> #_DOCS_SHOW converter.unregisterSubConverter(mxlConverter)
458+
>>> #_DOCS_SHOW mxlConverter in c.subConvertersList()
459+
>>> False #_DOCS_HIDE -- this breaks on parallel runs
455460
False
456461
457462
If there is no such subConverter registered, and it is not a default subConverter,
@@ -460,18 +465,25 @@ def unregisterSubConverter(
460465
>>> class ConverterSonix(converter.subConverters.SubConverter):
461466
... registerFormats = ('sonix',)
462467
... registerInputExtensions = ('mus',)
468+
463469
>>> converter.unregisterSubConverter(ConverterSonix)
464470
Traceback (most recent call last):
465-
music21.converter.ConverterException: Could not remove <class 'music21.ConverterSonix'> from
471+
music21.converter.ConverterException: Could not remove <class '...ConverterSonix'> from
466472
registered subConverters
467473
468474
The special command "all" removes everything including the default converters:
469475
470-
>>> converter.unregisterSubConverter('all')
471-
>>> c.subConvertersList()
476+
>>> #_DOCS_SHOW converter.unregisterSubConverter('all')
477+
>>> #_DOCS_SHOW c.subConvertersList()
478+
>>> [] #_DOCS_HIDE
472479
[]
473480
474-
>>> converter.resetSubConverters() #_DOCS_HIDE
481+
Housekeeping: let's reset our subconverters and check things are okay again.
482+
483+
>>> converter.resetSubConverters()
484+
>>> c = converter.Converter()
485+
>>> mxlConverter in c.subConvertersList()
486+
True
475487
'''
476488
if removeSubConverter == 'all':
477489
_registeredSubConverters.clear()
@@ -594,7 +606,7 @@ def getFormatFromFileExtension(self, fp):
594606
else:
595607
useFormat = common.findFormatFile(fp)
596608
if useFormat is None:
597-
raise ConverterFileException(f'cannot find a format extensions for: {fp}')
609+
raise ConverterFileException(f'cannot find format from file extensions for: {fp}')
598610
return useFormat
599611

600612
# noinspection PyShadowingBuiltins
@@ -861,7 +873,7 @@ def subConvertersList(
861873
>>> ConverterSonix in c.subConvertersList()
862874
True
863875
864-
Newly registered subConveters appear first, so they will be used instead
876+
Newly registered subConverters appear first, so they will be used instead
865877
of any default subConverters that work on the same format or extension.
866878
867879
>>> class BadMusicXMLConverter(converter.subConverters.SubConverter):
@@ -872,7 +884,7 @@ def subConvertersList(
872884
873885
>>> converter.registerSubConverter(BadMusicXMLConverter)
874886
>>> c.subConvertersList()
875-
[<class 'music21.BadMusicXMLConverter'>,
887+
[<class '...BadMusicXMLConverter'>,
876888
...
877889
<class 'music21.converter.subConverters.ConverterMusicXML'>,
878890
...]
@@ -896,6 +908,8 @@ def subConvertersList(
896908
>>> #_DOCS_SHOW s = corpus.parse('beach/prayer_of_a_tired_child')
897909
>>> s.id
898910
'empty'
911+
>>> len(s.parts)
912+
0
899913
>>> s = corpus.parse('beach/prayer_of_a_tired_child', forceSource=True)
900914
>>> len(s.parts)
901915
6
@@ -1512,7 +1526,6 @@ def freezeStr(streamObj, fmt=None):
15121526
the `fmt` argument; 'pickle' (the default),
15131527
is the only one presently supported.
15141528
1515-
15161529
>>> c = converter.parse('tinyNotation: 4/4 c4 d e f', makeNotation=False)
15171530
>>> c.show('text')
15181531
{0.0} <music21.meter.TimeSignature 4/4>
@@ -1530,7 +1543,6 @@ def freezeStr(streamObj, fmt=None):
15301543
{1.0} <music21.note.Note D>
15311544
{2.0} <music21.note.Note E>
15321545
{3.0} <music21.note.Note F>
1533-
15341546
'''
15351547
from music21 import freezeThaw
15361548
v = freezeThaw.StreamFreezer(streamObj)
@@ -1570,7 +1582,8 @@ def testMusicXMLConversion(self):
15701582
from music21.musicxml import testFiles
15711583
for mxString in testFiles.ALL:
15721584
a = subConverters.ConverterMusicXML()
1573-
a.parseData(mxString)
1585+
a.parseData(mxString.strip())
1586+
break
15741587

15751588

15761589
class TestExternal(unittest.TestCase):

music21/features/base.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import unittest
1919

2020
from music21 import common
21+
from music21.common.parallel import safeToParallize
2122
from music21.common.types import StreamType
2223
from music21 import converter
2324
from music21 import corpus
@@ -931,7 +932,7 @@ def process(self):
931932
Process all Data with all FeatureExtractors.
932933
Processed data is stored internally as numerous Feature objects.
933934
'''
934-
if self.runParallel:
935+
if self.runParallel and safeToParallize():
935936
return self._processParallel()
936937
else:
937938
return self._processNonParallel()
@@ -947,10 +948,10 @@ def _processParallel(self):
947948

948949
# print('about to run parallel')
949950
outputData = common.runParallel([(di, self.failFast) for di in self.dataInstances],
950-
_dataSetParallelSubprocess,
951-
updateFunction=shouldUpdate,
952-
updateMultiply=1,
953-
unpackIterable=True
951+
_dataSetParallelSubprocess,
952+
updateFunction=shouldUpdate,
953+
updateMultiply=1,
954+
unpackIterable=True
954955
)
955956
featureData, errors, classValues, ids = zip(*outputData)
956957
errors = common.flattenList(errors)

music21/figuredBass/harmony.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class FiguredBass(Harmony):
6060
>>> fb.pitches
6161
()
6262
63-
* new in v9.3
63+
* New in v9.3
6464
'''
6565
def __init__(self,
6666
figureString: str = '',

music21/metadata/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2399,6 +2399,8 @@ def convertValue(uniqueName: str, value: t.Any) -> ValueType:
23992399
>>> metadata.Metadata.convertValue('dateCreated',
24002400
... metadata.DateBetween(['1938', '1939']))
24012401
<music21.metadata.primitives.DateBetween 1938/--/-- to 1939/--/-->
2402+
2403+
* Added in v10 -- newly exposed as a public function (was private)
24022404
'''
24032405
valueType: type[ValueType]|None = properties.UNIQUE_NAME_TO_VALUE_TYPE.get(
24042406
uniqueName, None

0 commit comments

Comments
 (0)