Source code for music21.sites

# ------------------------------------------------------------------------------
# Name:         sites.py
# Purpose:      Objects for keeping track of relationships among Music21Objects
#
# Authors:      Michael Scott Asato Cuthbert
#               Christopher Ariza
#
# Copyright:    Copyright © 2007-2015 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
sites.py -- Objects for keeping track of relationships among Music21Objects
'''
from __future__ import annotations

import collections
from collections.abc import Generator, MutableMapping
import typing as t
from typing import overload  # for some reason does not work in PyCharm if not directly imported
import unittest
import weakref

from music21 import common
from music21 import exceptions21
from music21 import prebase


if t.TYPE_CHECKING:
    from music21 import stream


# define whether weakrefs are used for storage of object locations
WEAKREF_ACTIVE = True

# DEBUG_CONTEXT = False

# Global site state dict is a weakValueDictionary -- meaning that the values
# are weakrefs and the key, value pair disappears if the value
# vanishes.  It is used to store siteRef.siteWeakref values during a
# __setstate__, __getstate__ instance within the same Python session.
# if the object being pickled and then unpickled had a weakref to an object
# that still exists, then restore it from the dictionary; otherwise, do not
# sweat it.  Should make pickle deepcopies of music21 objects in Streams still
# possible without needing to recreate the whole stream.
GLOBAL_SITE_STATE_DICT: MutableMapping[str, t.Any|None] = weakref.WeakValueDictionary()


class SitesException(exceptions21.Music21Exception):
    pass

# -----------------------------------------------------------------------------


[docs] class SiteRef(common.SlottedObjectMixin, prebase.ProtoM21Object): ''' A single Site (stream, container, parent, reference, etc.) stored inside the Sites object. A very simple object. This site would be stored in the .sites.siteDict for, say a note.Note, if it were in st at offset 20.0 >>> st = stream.Stream() >>> st.id = 'hi' >>> s = sites.SiteRef() >>> s.classString = st.classes[0] >>> s.site = st >>> s.isDead False If you call s.site, you always get an object out, but internally, there's a .siteWeakref that stores a weakref to the site. >>> s.site <music21.stream.Stream hi> >>> s.siteWeakref <weakref at 0x123456; to ...Stream' at 0x111111> If you turn sites.WEAKREF_ACTIVE to False then .siteWeakref just stores another reference to the site. Bad for memory. Good for debugging pickling. OMIT_FROM_DOCS Note that the ...Stream was needed after Python 3.13. When 3.13 is the lowest version, replace ... with the actual output. ''' # CLASS VARIABLES # __slots__ = ( 'classString', 'globalSiteIndex', 'siteIndex', 'isDead', 'siteWeakref', ) # INITIALIZER # def __init__(self): self.isDead = False self.classString = None self.globalSiteIndex = None self.siteIndex = None self.siteWeakref = None def _reprInternal(self): if self is _NoneSiteRef: return 'Global None Index' siteRepr = repr(self.site) if self.isDead: siteRepr = 'dead site' return f'{self.siteIndex}/{self.globalSiteIndex} to {siteRepr}' def _getAndUnwrapSite(self): if WEAKREF_ACTIVE: ret = common.unwrapWeakref(self.siteWeakref) else: ret = self.siteWeakref if ret is None and self is not _NoneSiteRef: self.isDead = True return ret def _setAndWrapSite(self, site): if WEAKREF_ACTIVE: self.siteWeakref = common.wrapWeakref(site) else: self.siteWeakref = site self.isDead = False site = property(_getAndUnwrapSite, _setAndWrapSite) # called before pickling. def __getstate__(self): currentSite = None if WEAKREF_ACTIVE: currentSite = self.site if currentSite is None: self.siteWeakref = None else: siteIdValue = str(id(currentSite)) + '_' + str(_singletonCounter()) try: GLOBAL_SITE_STATE_DICT[siteIdValue] = currentSite except TypeError as te: raise TypeError( f'This str screwed up everything: {currentSite}' ) from te self.siteWeakref = siteIdValue returnState = common.SlottedObjectMixin.__getstate__(self) if WEAKREF_ACTIVE and currentSite is not None: self.site = currentSite return returnState # called on unpickling def __setstate__(self, state): common.SlottedObjectMixin.__setstate__(self, state) if WEAKREF_ACTIVE and self.siteWeakref is not None: siteIdValue = self.siteWeakref try: currentSite = GLOBAL_SITE_STATE_DICT[siteIdValue] del GLOBAL_SITE_STATE_DICT[siteIdValue] except KeyError: currentSite = None self.isDead = True self.site = currentSite
_NoneSiteRef = SiteRef() _NoneSiteRef.globalSiteIndex = -2 # -1 is used elsewhere _NoneSiteRef.siteIndex = -2 _singletonCounter = common.SingletonCounter()
[docs] class Sites(common.SlottedObjectMixin): ''' An object, stored within a Music21Object, that stores (weak) references to a collection of objects that may be contextually relevant to this object. Most of these objects are locations (also called sites), or Streams that contain this object. All defined contexts are stored as dictionaries in a dictionary. The outermost dictionary stores objects. ''' # CLASS VARIABLES # __slots__ = ( 'siteDict', '_lastID', '_siteIndex', ) # INITIALIZER # def __init__(self): # .siteDict is a dictionary of siteRefs. None is a singleton. self.siteDict = collections.OrderedDict([(None, _NoneSiteRef), ]) # store an index of numbers for tagging the order of creation of defined contexts; # this is used to be able to discern the order of context as added self._siteIndex = 0 # cache for performance self._lastID = -1 # cannot be None # SPECIAL METHODS # def __deepcopy__(self, memo=None): ''' Helper function for copy.deepcopy that in addition to copying produces a new, independent Sites object. This does not, however, deepcopy site references stored therein. All sites, however, are passed on to the new deepcopy, which means that in a deepcopy of a Stream that contains Notes, the copied Note will have the former site as a location, even though the new Note instance is not actually found in the old Stream. >>> import copy >>> class Mock(base.Music21Object): ... pass >>> aObj = Mock() >>> aContexts = sites.Sites() >>> aContexts.add(aObj) >>> bContexts = copy.deepcopy(aContexts) >>> len(aContexts.get()) == 2 True >>> len(bContexts.get()) == 2 True >>> aContexts.get() == bContexts.get() True ''' # TODO: it may be a problem that sites are being transferred to deep # copies; this functionality is used at times in context searches, but # may be a performance hog. new = self.__class__() # environLocal.printDebug(['Sites.__deepcopy__', # 'self.siteDict.keys()', self.siteDict.keys()]) for idKey in self.siteDict: if idKey is None: continue oldSite = self.siteDict[idKey] if oldSite.isDead: continue # do not copy dead references newSite = SiteRef() newSite.site = oldSite.site if oldSite.site is None: newIdKey = None else: newIdKey = id(newSite.site) newSite.siteIndex = oldSite.siteIndex newSite.globalSiteIndex = _singletonCounter() newSite.classString = oldSite.classString newSite.isDead = False # debug # originalObj = post.site # if id(originalObj) != idKey and originalObj is not None: # print(idKey, id(originalObj)) new.siteDict[newIdKey] = newSite new._siteIndex = self._siteIndex # keep the _siteIndex to stay coherent return new def __len__(self): ''' Return the total number of references. >>> class Mock(base.Music21Object): ... pass >>> aObj = Mock() >>> aContexts = sites.Sites() >>> len(aContexts) 1 >>> aContexts.add(aObj) >>> len(aContexts) 2 ''' return len(self.siteDict) def __contains__(self, checkSite): ''' Returns True if checkSite in Sites. >>> m1 = stream.Measure(number=1) >>> m2 = stream.Measure(number=2) >>> n = note.Note() >>> m1.append(n) >>> m1 in n.sites True >>> m2 in n.sites False None is always in sites >>> None in n.sites True ''' for unused_siteRefId, siteRef in self.siteDict.items(): if siteRef.site is checkSite: return True return False def __iter__(self): ''' Returns all non-None sites. Order is oldest first. >>> n = note.Note() >>> m = stream.Measure(number=1) >>> s = stream.Stream(id='thisStream') >>> m.insert(10, n) >>> s.insert(20, n) >>> for site in n.sites: ... print(site, n.getOffsetBySite(site)) <music21.stream.Measure 1 offset=0.0> 10.0 <music21.stream.Stream thisStream> 20.0 ''' return self.yieldSites(excludeNone=True) # PUBLIC METHODS #
[docs] def add(self, obj, timeValue=None, idKey=None, classString=None): ''' Add a reference to the `Sites` collection for this object. Automatically called on stream.insert(n), etc. `idKey` stores the id() of the obj. If `None`, then id(obj) is used. `classString` stores the class of obj. If `None` then `obj.classes[0]` is used. TODO: Tests. Including updates. ''' if timeValue is not None: raise SitesException('No timeValue in sites anymore!') # NOTE: this is a performance critical method # a None object will have a key of None # do not need to set this as is default if idKey is None and obj is not None: idKey = id(obj) updateNotAdd = False if idKey in self.siteDict: tempSiteRef = self.siteDict[idKey] if (tempSiteRef.isDead is False and tempSiteRef.site is not None): updateNotAdd = True # if idKey is not None: # print( # 'Updating idKey ' # f'{idKey} for object {id(obj)}' # ) # environLocal.printDebug(['adding obj', obj, idKey]) # weak refs were being passed in __deepcopy__ calling this method # __deepcopy__ no longer call this method, so we can assume that # we will not get weakrefs if obj is not None and classString is None: classString = obj.classes[0] # get most current class if updateNotAdd is True: siteRef = self.siteDict[idKey] siteRef.isDead = False # in case it used to be a dead site else: siteRef = SiteRef() siteRef.site = obj # stores a weakRef siteRef.classString = classString # default # singleContextDict.isDead = False # store to access w/o unwrapping # time is a numeric count, not a real time measure siteRef.siteIndex = self._siteIndex self._siteIndex += 1 # increment for next usage siteRef.globalSiteIndex = _singletonCounter() # increments ## if not updateNotAdd: # add new/missing information to dictionary self.siteDict[idKey] = siteRef
[docs] def clear(self): ''' Clear all stored data. ''' self.siteDict = collections.OrderedDict([(None, _NoneSiteRef), ]) self._lastID = -1 # cannot be None
@overload def yieldSites(self, *, excludeNone: t.Literal[True], sortByCreationTime: bool|t.Literal['reverse'] = False, priorityTarget=None, ) -> Generator[stream.Stream, None, None]: ... @overload def yieldSites(self, *, excludeNone: bool = False, sortByCreationTime: bool|t.Literal['reverse'] = False, priorityTarget=None, ) -> Generator[stream.Stream|None, None, None]: ...
[docs] def yieldSites(self, *, excludeNone: bool = False, sortByCreationTime: bool|t.Literal['reverse'] = False, priorityTarget=None, ) -> Generator[stream.Stream|None, None, None]: # noinspection PyDunderSlots ''' Yield references; order, based on dictionary keys, is from least recently added to most recently added. The `sortByCreationTime` option if set to True will sort objects by creation time, where most-recently assigned objects are returned first. Note that priorityTarget is searched only on id -- this could be dangerous if the target has been garbage collected and the id is reused. Unlikely since you have to pass in the priorityTarget itself, so therefore it still exists. This can be much faster than .get in the case where the sought-for site is earlier in the list. >>> class Mock(base.Music21Object): ... def __init__(self, idEl): ... self.id = idEl >>> aObj = Mock('a') >>> bObj = Mock('b') >>> cObj = Mock('c') >>> aSites = sites.Sites() >>> aSites.add(cObj) >>> aSites.add(aObj) >>> aSites.add(bObj) Returns a generator: >>> ys = aSites.yieldSites() >>> ys <generator object Sites.yieldSites at 0x1058085e8> That's no help, so iterate over it instead: >>> for s in aSites.yieldSites(sortByCreationTime=True, excludeNone=True): ... print(s.id) b a c With priorityTarget >>> for s in aSites.yieldSites(sortByCreationTime=True, priorityTarget=cObj, ... excludeNone=True): ... print(s.id) c b a * Changed in v3: changed dramatically from previously unused version `sortByCreationTime='reverse'` is removed, since the ordered dict takes care of it and was not working. * Changed in v8: arguments are keyword only. ''' keyRepository = list(self.siteDict.keys()) if sortByCreationTime is True: keyRepository.reverse() if priorityTarget is not None: priorityId = id(priorityTarget) if priorityId in keyRepository: # environLocal.printDebug(['priorityTarget found in post:', priorityTarget]) # extract object and make first keyRepository.insert(0, keyRepository.pop(keyRepository.index(priorityId))) # get each dict from all defined contexts for key in keyRepository: siteRef = self.siteDict[key] # check for None object; default location, not a weakref, keep if siteRef.site is None: if not excludeNone: yield siteRef.site else: obj = siteRef.site if obj is None: # dead ref siteRef.isDead = True else: yield obj
[docs] def get(self, *, sortByCreationTime=False, priorityTarget=None, excludeNone=False): ''' Get references; order, based on dictionary keys, is from most recently added to least recently added. The `sortByCreationTime` option will sort objects by creation time, where most-recently assigned objects are returned first. Can be [False, other], [True, 1] or ['reverse', -1] If `priorityTarget` is defined, this object will be placed first in the list of objects. >>> class Mock(base.Music21Object): ... pass >>> aObj = Mock() >>> bObj = Mock() >>> cObj = Mock() >>> aSites = sites.Sites() >>> aSites.add(cObj) >>> aSites.add(aObj) >>> aSites.add(bObj) Arbitrary order, so we compare with sets: >>> set(aSites.get()) == {None, cObj, aObj, bObj} True Particular order, with None at the end. >>> aSites.get(sortByCreationTime=True) == [bObj, aObj, cObj, None] True Priority target >>> begotten = aSites.get(sortByCreationTime=True, priorityTarget=cObj, excludeNone=True) >>> begotten == [cObj, bObj, aObj] True * Changed in v5.5: keyword only. ''' post = list(self.yieldSites(sortByCreationTime=sortByCreationTime, priorityTarget=priorityTarget, excludeNone=excludeNone)) # we do this resorting again, because the priority target might not match id, and we # want to be extra safe. If you want fast, use .yieldSites if priorityTarget is not None: if priorityTarget in post: # environLocal.printDebug(['priorityTarget found in post:', priorityTarget]) # extract object and make first post.insert(0, post.pop(post.index(priorityTarget))) return post
[docs] def getAttrByName(self, attrName): ''' Given an attribute name, search all objects and find the first that matches this attribute name; then return a reference to this attribute. Works in reverse order, so most recent site is returned first. >>> class Mock(base.Music21Object): ... attr1 = 234 >>> aObj = Mock() >>> aObj.attr1 = 234 >>> bObj = Mock() >>> bObj.attr1 = 98 >>> aSites = sites.Sites() >>> len(aSites) 1 >>> aSites.add(aObj) >>> len(aSites) 2 >>> aSites.getAttrByName('attr1') == 234 True >>> aSites.remove(aObj) >>> aSites.add(bObj) >>> aSites.getAttrByName('attr1') == 98 True An incorrect attribute name will just give none: >>> aSites.getAttrByName('blah') is None True ''' post = None for obj in self.yieldSites(sortByCreationTime='reverse'): if obj is None: continue # in case the reference is dead try: post = getattr(obj, attrName) return post except AttributeError: pass
[docs] def getObjByClass( self, className, *, callerFirst=None, sortByCreationTime=False, priorityTarget=None, getElementMethod=common.enums.ElementSearch.AT_OR_BEFORE, memo=None ): ''' Return the most recently added reference based on className. Class name can be a string or the class name. This will recursively search the sitesDicts of objects in Site objects in the siteDict. The `callerFirst` parameter is simply used to pass a reference of the first caller; this is necessary if we are looking within a Stream for a flat offset position. If `priorityTarget` is specified, this location will be searched first. Use priorityTarget=activeSite to prioritize that. The `getElementMethod` is a string that selects which Stream method is used to get elements for searching with getElementsByClass() calls. >>> import time >>> class Mock(base.Music21Object): ... pass >>> aObj = Mock() >>> bObj = Mock() >>> aSites = sites.Sites() >>> aSites.add(aObj) >>> aSites.add(bObj) We get the most recently added object first >>> aSites.getObjByClass('Mock', sortByCreationTime=True) == bObj True >>> aSites.getObjByClass(Mock, sortByCreationTime=True) == bObj True * Changed in v5.5: all arguments except className are keyword only. OMIT_FROM_DOCS TODO: not sure if memo is properly working: need a test case ''' # in general, this should not be the first caller, as this method # is called from a Music21Object, not directly on the Sites # instance. Nonetheless, if this is the first caller, it is the first # caller. if callerFirst is None: # this is the first caller callerFirst = self # set Sites as caller first if memo is None: memo = {} # initialize post = None # count = 0 # search any defined contexts first # need to sort: look at most-recently added objs are first objs = self.yieldSites( sortByCreationTime=sortByCreationTime, priorityTarget=priorityTarget, excludeNone=True, ) # objs is a generator # printMemo( # memo, # 'getObjByClass() called: looking at ' # + f'{len(objs)} sites' # ) classNameIsStr = isinstance(className, str) for obj in objs: # environLocal.printDebug(['memo', memo]) if classNameIsStr: if className in obj.classes: post = obj break elif isinstance(obj, className): post = obj break if post is not None: return post # all objs here are containers, as they are all locations # if we could be sure that these objs do not have their own locations # and do not have the target class, we can skip for obj in objs: # if DEBUG_CONTEXT: print('\tY: getObjByClass: iterating objs:', id(obj), obj) if classNameIsStr and obj.isFlat: # if DEBUG_CONTEXT: # print('\tY: skipping flat stream that does not contain object:', # id(obj), obj) # environLocal.printDebug( # ['\tY: skipping flat stream that does not contain object:']) if obj.sites.getSiteCount() == 0: # is top level; no more to search if not obj.getElementsByClass(className): continue # skip, not in this stream # if after trying to match name, look in the defined contexts' # defined contexts [sic!] # if post is None: # no match yet # access public method to recurse if id(obj) not in memo: # if the object is a Music21Object # if hasattr(obj, 'getContextByClass'): # store this object as having been searched memo[id(obj)] = obj post = obj.getContextByClass( className, sortByCreationTime=sortByCreationTime, getElementMethod=getElementMethod) if post is not None: break return post
[docs] def getById(self, siteId): ''' Return the object specified by an id. Used for testing and debugging. Should NOT be used in production code. >>> a = note.Note() >>> s = stream.Stream() >>> s.append(a) >>> a.sites.getById(id(s)) is s True ''' siteRef = self.siteDict[siteId] return siteRef.site
[docs] def getSiteCount(self): ''' Return the number of non-dead sites, excluding the None site. This does not unwrap weakrefs for performance. >>> a = note.Note() >>> a.sites.getSiteCount() 0 >>> s = stream.Stream() >>> s.append(a) >>> a.sites.getSiteCount() 1 >>> sf = s.flatten() >>> a.sites.getSiteCount() 2 ''' count = 0 for siteRef in self.siteDict.values(): if siteRef.isDead is True: continue if siteRef.siteWeakref is None: continue count += 1 return count
[docs] def getSiteIds(self): ''' Return a set of all site Ids. >>> class Mock(base.Music21Object): ... pass >>> aSite = Mock() >>> dc = sites.Sites() >>> dc.add(aSite) >>> dc.getSiteIds() == {None, id(aSite)} True ''' # may want to convert to tuple to avoid user editing? return set(self.siteDict.keys())
[docs] def getSitesByClass(self, className): ''' Return a list of unwrapped site from siteDict.site [SiteRef.site] (generally a Stream) that matches the provided class. Input can be either a Class object or a string. >>> class Mock(base.Music21Object): ... pass >>> aObj = Mock() >>> bObj = Mock() >>> cObj = stream.Stream() >>> aSites = sites.Sites() >>> aSites.add(aObj) >>> aSites.add(bObj) >>> aSites.add(cObj) >>> aSites.getSitesByClass(Mock) == [aObj, bObj] True >>> aSites.getSitesByClass('Stream') == [cObj] True ''' found = [] if not isinstance(className, str): className = common.classToClassStr(className) for siteRef in self.siteDict.values(): if siteRef.isDead: continue classStr = siteRef.classString if classStr == className: objRef = siteRef.site found.append(objRef) return found
[docs] def hasSiteId(self, siteId): ''' Return True or False if this Sites object already has this site id. >>> class Mock(base.Music21Object): ... pass >>> aSite = Mock() >>> bSite = Mock() >>> dc = sites.Sites() >>> dc.add(aSite) >>> dc.hasSiteId(id(aSite)) True >>> dc.hasSiteId(id(bSite)) False Note that we use 'None' not id(None) as a key: >>> dc.hasSiteId(id(None)) False >>> dc.hasSiteId(None) True ''' return siteId in self.siteDict
[docs] def hasSpannerSite(self): ''' Return True if this object is found in any Spanner. This is determined by looking for a SpannerStorage Stream class as a Site. ''' for siteRef in self.siteDict.values(): if siteRef.isDead: continue if siteRef.classString == 'SpannerStorage': return True return False
[docs] def hasVariantSite(self): ''' Return True if this object is found in any Variant. This is determined by looking for a VariantStorage Stream class as a Site. ''' for siteRef in self.siteDict.values(): if siteRef.isDead: continue if siteRef.classString == 'VariantStorage': return True return False
[docs] def purgeLocations(self, rescanIsDead=False): ''' Clean all locations that refer to objects that no longer exist. The `removeOrphanedSites` option removes sites that may have been the result of deepcopy: the element has the site, but the site does not have the element. This results b/c Sites are shallow-copied, and then elements are re-added. >>> import gc >>> class Mock(base.Music21Object): ... pass >>> aStream = stream.Stream() >>> bStream = stream.Stream() >>> mySites = sites.Sites() >>> len(mySites) 1 >>> mySites.add(aStream) >>> mySites.add(bStream) >>> del aStream >>> numObjectsCollected = gc.collect() # make sure to garbage collect We still have 3 locations -- just because aStream is gone, doesn't make it disappear from sites >>> len(mySites) 3 >>> mySites.purgeLocations(rescanIsDead=True) >>> len(mySites) 2 ''' # first, check if any sites are dead, and cache the results if rescanIsDead: for idKey in self.siteDict: if idKey is None: continue siteRef = self.siteDict[idKey] obj = siteRef.site if obj is None: # if None, it no longer exists siteRef.isDead = True else: siteRef.isDead = False # use previously set isDead entry, so as not to # unwrap all references remove = [] for idKey in self.siteDict: if idKey is None: continue siteRef = self.siteDict[idKey] if siteRef.isDead: remove.append(idKey) for idKey in remove: # this call changes the .siteDict, and thus must be # outside the loop self.removeById(idKey)
[docs] def remove(self, site): ''' Remove the object (a context or location site) specified from Sites. Object provided can be a location site (i.e., a Stream) or a pure context (like a Temperament). N.B. -- like all .sites operations, this is an advanced tool not for standard music21 usage. Instead of: elObj.remove(streamObj) use this command, which will take care of `.sites.remove` as well as removing `elObj` from `streamObj.elements`: streamObj.remove(elObj) >>> class Mock(base.Music21Object): ... pass >>> aSite = Mock() >>> bSite = Mock() >>> cSite = Mock() >>> aSites = sites.Sites() >>> len(aSites) 1 >>> aSites.add(aSite) >>> len(aSites) 2 >>> aSites.add(bSite) >>> len(aSites) 3 >>> aSites.add(cSite) >>> len(aSites) 4 >>> aSites.remove(aSite) >>> len(aSites) 3 ''' # must clear self._lastID = -1 # cannot be None siteId = None if site is not None: siteId = id(site) try: del self.siteDict[siteId] # environLocal.printDebug(['removed site w/o exception:', siteId, # 'self.siteDict.keys()', self.siteDict.keys()]) except Exception as e: raise SitesException( 'an entry for this object ' f'({site}) is not stored in this Sites object' ) from e
[docs] def removeById(self, idKey): ''' Remove a site entry by id key, which is id() of the object. ''' # must clear if removing if idKey == self._lastID: self._lastID = -1 # cannot be None if idKey is None: raise SitesException('trying to remove None idKey is not allowed') # environLocal.printDebug(['removeById', idKey, # 'self.siteDict.keys()', self.siteDict.keys()]) try: del self.siteDict[idKey] except KeyError: pass # could already be gone.
[docs] def setAttrByName(self, attrName, value): ''' Given an attribute name, search all objects and find the first that matches this attribute name; then return a reference to this attribute. >>> class Mock(base.Music21Object): ... attr1 = 234 >>> aObj = Mock() >>> bObj = Mock() >>> bObj.attr1 = 98 >>> aSites = sites.Sites() >>> aSites.add(aObj) >>> aSites.add(bObj) >>> aSites.setAttrByName('attr1', 'test') >>> aSites.getAttrByName('attr1') == 'test' True ''' # post = None for obj in self.get(): if obj is None: continue # in case the reference is dead try: junk = getattr(obj, attrName) # if attr already exists setattr(obj, attrName, value) # if attr already exists except AttributeError: pass
class Test(unittest.TestCase): def testSites(self): from music21 import note from music21 import stream from music21 import corpus from music21 import clef m = stream.Measure() m.number = 34 n = note.Note() m.append(n) n2 = note.Note() n2.sites.add(m) c = clef.Clef() c.sites.add(n) self.assertEqual(n2.sites.getAttrByName('number'), 34) c.sites.setAttrByName('lyric', str(n2.sites.getAttrByName('number'))) self.assertEqual(n.lyric, '34') c.sites.setAttrByName('lyric', n2.sites.getAttrByName('number')) # converted to a string now self.assertEqual(n.lyric, '34') violin1 = corpus.parse( 'beethoven/opus18no1', 3, fileExtensions=('xml',), ).getElementById('Violin I') lastNote = violin1.flatten().notes[-1] lastNoteClef = lastNote.getContextByClass(clef.Clef) self.assertIsInstance(lastNoteClef, clef.TrebleClef) # ---------------------------------------------------------------------------- _DOC_ORDER = [SiteRef, Sites] # ----------------------------------------------------------------------------- if __name__ == '__main__': import music21 music21.mainTest(Test)