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):