diff --git a/.idea/pyProjectModel.xml b/.idea/pyProjectModel.xml
new file mode 100644
index 0000000000..d3679d8f6c
--- /dev/null
+++ b/.idea/pyProjectModel.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/music21/configure.py b/music21/configure.py
index e3c24e4ad4..e5e3ad3c1b 100644
--- a/music21/configure.py
+++ b/music21/configure.py
@@ -3,8 +3,9 @@
# Purpose: Installation and Configuration Utilities
#
# Authors: Christopher Ariza
+# Michael Scott Asato Cuthbert
#
-# Copyright: Copyright © 2011-2019 Michael Scott Asato Cuthbert
+# Copyright: Copyright © 2011-2026 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
from __future__ import annotations
@@ -41,6 +42,9 @@
reFinaleReaderApp = re.compile(r'Finale Reader\.app', IGNORECASE)
reMuseScoreApp = re.compile(r'MuseScore.*\.app', IGNORECASE)
reMuseScoreExe = re.compile(r'Musescore.*\\bin\\MuseScore.*\.exe', IGNORECASE)
+# matches any MuseScore file/app name regardless of version or platform:
+# MuseScore.app, MuseScore 4.app, MuseScore3.exe, mscore, mscore4portable, etc.
+reMuseScoreName = re.compile(r'MuseScore|mscore', IGNORECASE)
reDoricoApp = re.compile(r'Dorico.*\.app', IGNORECASE)
reDoricoExe = re.compile(r'Dorico.*\.exe', IGNORECASE)
@@ -1293,17 +1297,21 @@ def _askFillEmptyList(self, default=None, force=None):
def _performAction(self, simulate=False):
'''
- The action here is to open the stored URL in a browser, if the user agrees.
+ Set musicxmlPath to the selected reader. If the reader is a version
+ of MuseScore, also set musescoreDirectPNGPath to it.
'''
result = self.getResult()
if result is not None and not isinstance(result, DialogError):
# noinspection PyTypeChecker
reload(environment)
- # us = environment.UserSettings()
- # us['musicxmlPath'] = result # automatically writes
environment.set('musicxmlPath', result)
musicXmlNew = environment.get('musicxmlPath')
- self._writeToUser([f'MusicXML Reader set to: {musicXmlNew}', ' '])
+ if reMuseScoreName.search(pathlib.Path(str(result)).name):
+ environment.set('musescoreDirectPNGPath', musicXmlNew)
+ self._writeToUser(
+ [f'MusicXML Reader and PNG/PDF generator set to: {musicXmlNew}', ' '])
+ else:
+ self._writeToUser([f'MusicXML Reader set to: {musicXmlNew}', ' '])
# ------------------------------------------------------------------------------
@@ -1579,6 +1587,18 @@ def getValidResults(force=None):
# returns a bad condition b/c there are no options and user entered 'n'
self.assertIsInstance(post, configure.BadConditions)
+ def testMuseScoreNameRe(self):
+ '''
+ AI-assisted (Claude).
+ '''
+ for name in ('MuseScore.app', 'MuseScore 4.app', 'MuseScore 4.5.app',
+ 'MuseScore3.exe', 'MuseScore4.exe', 'MuseScore Studio.app',
+ 'mscore', 'mscore3', 'mscore4portable'):
+ self.assertIsNotNone(reMuseScoreName.search(name), name)
+ for name in ('Finale 2011.app', 'Finale.exe', 'Sibelius.app',
+ 'Dorico 5.app', 'Finale Reader.app'):
+ self.assertIsNone(reMuseScoreName.search(name), name)
+
def testRe(self):
g = reFinaleApp.search('Finale 2011.app')
self.assertEqual(g.group(0), 'Finale 2011.app')
diff --git a/music21/environment.py b/music21/environment.py
index 0073ea8e48..dd724a2de5 100644
--- a/music21/environment.py
+++ b/music21/environment.py
@@ -33,7 +33,6 @@
from typing import overload
import xml.etree.ElementTree as ET
-from xml.sax import saxutils
from music21 import exceptions21
from music21 import common
@@ -234,11 +233,6 @@ def __repr__(self):
return ''
def __setitem__(self, key, value):
- # saxutils.escape # used for escaping strings going to xml
- # with unicode encoding
- # http://www.xml.com/pub/a/2002/11/13/py-xml.html?page=2
- # saxutils.escape(msg).encode('UTF-8')
-
# add local corpus path as a key
# if isinstance(value, bytes):
# value = value.decode(errors='replace')
@@ -275,9 +269,8 @@ def __setitem__(self, key, value):
raise EnvironmentException(
f'{value} is not an acceptable value for preference: {key}')
- # need to escape problematic characters for xml storage
- if isinstance(value, str):
- value = saxutils.escape(value) # .encode('UTF-8')
+ # ElementTree handles XML escaping when serializing in toSettingsXML.
+
# set value
if key == 'localCorpusPath':
# only add if unique
@@ -885,8 +878,8 @@ def __setitem__(self, key, value):
>>> a = environment.Environment()
>>> a['debug'] = 1
>>> a['graphicsPath'] = '/test&Encode'
- >>> 'test&Encode' in str(a['graphicsPath'])
- True
+ >>> str(a['graphicsPath'])
+ '/test&Encode'
>>> isinstance(a['graphicsPath'], Path)
True
@@ -902,6 +895,8 @@ def __setitem__(self, key, value):
>>> a['showFormat'] = 'musicxml'
>>> a['localCorpusPath'] = '/path/to/local'
+
+ * Fixed in v11: values containing ``&`` etc. are properly stored and retrieved.
'''
envSingleton().__setitem__(key, value)
@@ -1540,6 +1535,30 @@ def testToSettings(self):
'/System/Applications/Preview', '/Applications/Preview'), match)
self.assertTrue(common.whitespaceEqual(canonic, match) or true_but_for_preview_location)
+ @unittest.skipIf(common.getPlatform() == 'win', 'test assumes Unix-style paths')
+ def testSpecialCharactersRoundTrip(self):
+ '''
+ A path containing XML special characters such as "&" is stored
+ unescaped, serialized escaped by ElementTree, and comes back
+ unescaped after parsing.
+
+ AI-assisted (Claude).
+ '''
+ core = _EnvironmentCore(forcePlatform='darwin')
+ fp = '/Applications/Q&A /MuseScore 4.app'
+
+ # these lines use overridden __setitem__ and __getitem__ internally
+ core['musicxmlPath'] = fp
+ self.assertEqual(str(core['musicxmlPath']), fp)
+
+ settingsTree = core.toSettingsXML()
+ match = self.stringFromTree(settingsTree)
+ self.assertIn('Q&A <Beta>', match)
+
+ ref = {'musicxmlPath': None}
+ core._fromSettings(settingsTree, ref)
+ self.assertEqual(ref['musicxmlPath'], fp)
+
@unittest.skipIf(common.getPlatform() == 'win', 'test assumes Unix-style paths')
def testFromSettings(self):