Repository: cuthbertLab/music21
Pull request #1761 (2025-03-19 22:01:45)

Support enclosure == Enclosure.NO_ENCLOSURE which is different from enclosure == None


Patch:
diff --git a/music21/_version.py b/music21/_version.py
index 021236eba..93855a2e7 100644
--- a/music21/_version.py
+++ b/music21/_version.py
@@ -50,7 +50,7 @@ so change it if a bug or new feature creates a problem with using old pickles.
 '''
 from __future__ import annotations
 
-__version__ = '9.6.0b1'
+__version__ = '9.6.0b2'
 
 def get_version_tuple(vv):
     v = vv.split('.')
diff --git a/music21/base.py b/music21/base.py
index 4e6c0abfd..f44828fb9 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -27,7 +27,7 @@ available after importing `music21`.
 <class 'music21.base.Music21Object'>
 
 >>> music21.VERSION_STR
-'9.6.0b1'
+'9.6.0b2'
 
 Alternatively, after doing a complete import, these classes are available
 under the module "base":
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index d36be8ea1..171cdc807 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -808,8 +808,6 @@ class XMLExporterBase:
             except AttributeError:
                 continue
 
-            if m21Name in xmlObjects.STYLE_ATTRIBUTES_STR_NONE_TO_NONE and m21Value is None:
-                m21Value = 'none'
             if m21Name in xmlObjects.STYLE_ATTRIBUTES_YES_NO_TO_BOOL:
                 m21Value = xmlObjects.booleanToYesNo(m21Value)
 
@@ -6159,9 +6157,12 @@ class MeasureExporter(XMLExporterBase):
           </direction-type>
         </direction>
 
-        turn coda.useSymbol to `False` to get a text expression instead
+        turn coda.useSymbol to `False` to get a text expression instead,
+        and set enclosure to NO_ENCLOSURE to explicitly generate
+        enclosure="none".
 
         >>> c.useSymbol = False
+        >>> c.style.enclosure = style.Enclosure.NO_ENCLOSURE
         >>> MEX = musicxml.m21ToXml.MeasureExporter()
         >>> mxCodaText = MEX.codaToXml(c)
         >>> MEX.dump(mxCodaText)
@@ -6213,7 +6214,7 @@ class MeasureExporter(XMLExporterBase):
         >>> MEX.dump(MEX.xmlRoot.findall('direction')[1])
         <direction>
           <direction-type>
-            <words default-y="45" enclosure="none" font-weight="bold"
+            <words default-y="45" font-weight="bold"
                 justify="left">slow</words>
           </direction-type>
         </direction>
@@ -6283,7 +6284,7 @@ class MeasureExporter(XMLExporterBase):
         >>> MEX.dump(mxDirection)
         <direction>
           <direction-type>
-            <words default-y="45" enclosure="none" font-weight="bold">Andante</words>
+            <words default-y="45" font-weight="bold">Andante</words>
           </direction-type>
         </direction>
 
diff --git a/music21/musicxml/xmlObjects.py b/music21/musicxml/xmlObjects.py
index 6a07ef388..2aa313471 100644
--- a/music21/musicxml/xmlObjects.py
+++ b/music21/musicxml/xmlObjects.py
@@ -134,7 +134,6 @@ class MusicXMLWarning(UserWarning):
 # ------------------------------------------------------------------------------
 # helpers
 STYLE_ATTRIBUTES_YES_NO_TO_BOOL = ('hideObjectOnPrint', )
-STYLE_ATTRIBUTES_STR_NONE_TO_NONE = ('enclosure', )
 
 
 def yesNoToBoolean(value):
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 64d46cdd8..f29514d4d 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -319,9 +319,7 @@ class XMLParserBase:
             if mxValue is None:
                 continue
 
-            if mxValue == 'none' and m21Name in xmlObjects.STYLE_ATTRIBUTES_STR_NONE_TO_NONE:
-                mxValue = None
-            elif m21Name in xmlObjects.STYLE_ATTRIBUTES_YES_NO_TO_BOOL:
+            if m21Name in xmlObjects.STYLE_ATTRIBUTES_YES_NO_TO_BOOL:
                 mxValue = xmlObjects.yesNoToBoolean(mxValue)
 
             try:
diff --git a/music21/style.py b/music21/style.py
index f01501c25..a297a292d 100644
--- a/music21/style.py
+++ b/music21/style.py
@@ -45,7 +45,7 @@ class Enclosure(common.StrEnum):
     NONAGON = 'nonagon'
     DECAGON = 'decagon'
     INVERTED_BRACKET = 'inverted-bracket'
-    NONE = 'none'  # special -- sets to None.
+    NO_ENCLOSURE = 'none'
 
 
 class Style(ProtoM21Object):
@@ -87,6 +87,8 @@ class Style(ProtoM21Object):
         # managed by property below.
         self._absoluteY: float|int|None = None
 
+        # None means enclosure is left unspecified, NO_ENCLOSURE means
+        # explicitly that there is no enclosure.
         self._enclosure: Enclosure|None = None
 
         # how should this symbol be represented in the font?
@@ -105,7 +107,8 @@ class Style(ProtoM21Object):
     @property
     def enclosure(self) -> Enclosure|None:
         '''
-        Get or set the enclosure as a style.Enclosure enum or None.
+        Get or set the enclosure as a style.Enclosure enum (or
+        None, which means the enclosure is left unspecified).
 
         Valid names are:
 
@@ -115,7 +118,8 @@ class Style(ProtoM21Object):
         * "circle"/style.Enclosure.CIRCLE,
         * "bracket"/style.Enclosure.BRACKET,
         * "inverted-bracket"/style.Enclosure.INVERTED_BRACKET (output in musicxml 4 only)
-        * None/"none"/style.Enclosure.NONE (returns Python None object)
+        * "none"/style.Enclosure.NO_ENCLOSURE
+        * None (i.e. enclosure is unspecified)
 
         or the following other shapes with their ALLCAPS Enclosure equivalents:
 
@@ -151,15 +155,16 @@ class Style(ProtoM21Object):
         Traceback (most recent call last):
         music21.style.TextFormatException:
             Not a supported enclosure: 4
+
+        * Changed in v9.7: We now differentiate between no enclosure
+          (Enclosure.NO_ENCLOSURE) and unspecified enclosure (None).
         '''
         return self._enclosure
 
     @enclosure.setter
-    def enclosure(self, value: Enclosure|None):
+    def enclosure(self, value: Enclosure|str|None):
         if value is None:
             self._enclosure = value
-        elif value == Enclosure.NONE:
-            self._enclosure = None
         elif isinstance(value, Enclosure):
             self._enclosure = value
         elif isinstance(value, str):


Test patch:
diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py
index 5b1361bcc..dd4450bb0 100644
--- a/music21/musicxml/test_xmlToM21.py
+++ b/music21/musicxml/test_xmlToM21.py
@@ -23,6 +23,7 @@ from music21 import pitch
 from music21 import repeat
 from music21 import spanner
 from music21 import stream
+from music21 import style
 from music21 import tempo
 from music21 import text
 
@@ -1064,7 +1065,8 @@ class Test(unittest.TestCase):
         self.assertEqual(len(rmIterator), 4)
         self.assertEqual(rmIterator[0].content, 'A')
         self.assertEqual(rmIterator[1].content, 'B')
-        self.assertEqual(rmIterator[1].style.enclosure, None)
+        self.assertEqual(rmIterator[1].style.enclosure,
+                         style.Enclosure.NO_ENCLOSURE)
         self.assertEqual(rmIterator[2].content, 'Test')
         self.assertEqual(rmIterator[2].style.enclosure, 'square')