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
6 changes: 6 additions & 0 deletions .idea/pyProjectModel.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 25 additions & 5 deletions music21/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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}', ' '])


# ------------------------------------------------------------------------------
Expand Down Expand Up @@ -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')
Expand Down
41 changes: 30 additions & 11 deletions music21/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -234,11 +233,6 @@ def __repr__(self):
return '<Environment>'

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')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -885,8 +878,8 @@ def __setitem__(self, key, value):
>>> a = environment.Environment()
>>> a['debug'] = 1
>>> a['graphicsPath'] = '/test&Encode'
>>> 'test&amp;Encode' in str(a['graphicsPath'])
True
>>> str(a['graphicsPath'])
'/test&Encode'
>>> isinstance(a['graphicsPath'], Path)
True

Expand All @@ -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)

Expand Down Expand Up @@ -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 <Beta>/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&amp;A &lt;Beta&gt;', 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):

Expand Down
Loading