Source code for music21.derivation

# -----------------------------------------------------------------------------
# Name:         derivation.py
# Purpose:      Class for storing and managing Stream-based derivations
#
# Authors:      Christopher Ariza
#               Joséphine Wolf Oberholtzer
#               Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2011-2014 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ----------------------------------------------------------------------------
'''
This module defines objects for tracking the derivation of one
:class:`~music21.stream.Stream` from another.
'''
from __future__ import annotations

import weakref
from collections.abc import Generator
import functools
import typing as t
import unittest

from music21 import common
from music21.common.objects import SlottedObjectMixin
from music21 import environment


if t.TYPE_CHECKING:
    from music21 import base


environLocal = environment.Environment('derivation')


[docs] def derivationMethod(function): ''' This decorator can be used for creating a function that returns a new derivation. But is currently unused, since it does not take into account `inPlace=True`. `Stream.cloneEmpty(derivationMethod='derivationMethod')` is preferred for Streams. >>> from copy import deepcopy >>> @derivation.derivationMethod ... def allGreen(n): ... n2 = deepcopy(n) ... n2.style.color = 'green' ... return n2 >>> n = note.Note('C#') >>> n2 = allGreen(n) >>> n2.style.color 'green' >>> n2.name = 'D-' >>> n2.derivation <Derivation of <music21.note.Note D-> from <music21.note.Note C#> via 'allGreen'> ''' @functools.wraps(function) def wrapper(self, *arguments, **keywords): result = function(self, *arguments, **keywords) result.derivation.origin = self result.derivation.method = function.__name__ return result return wrapper
[docs] class Derivation(SlottedObjectMixin): ''' A Derivation object keeps track of which Streams (or perhaps other Music21Objects) a Stream or other music21 object has come from and how. Derivation is automatically updated by many methods: >>> import copy >>> sOrig = stream.Stream(id='orig') >>> sNew = copy.deepcopy(sOrig) >>> sNew.id = 'copy' >>> sNew.derivation <Derivation of <music21.stream.Stream copy> from <music21.stream.Stream orig> via '__deepcopy__'> >>> sNew.derivation.client <music21.stream.Stream copy> >>> sNew.derivation.client is sNew True >>> sNew.derivation.origin <music21.stream.Stream orig> >>> sNew.derivation.method '__deepcopy__' >>> s1 = stream.Stream() >>> s1.id = 'DerivedStream' >>> d1 = derivation.Derivation(s1) >>> s2 = stream.Stream() >>> s2.id = 'OriginalStream' >>> d1.method = 'manual' >>> d1.origin = s2 >>> d1 <Derivation of <music21.stream.Stream DerivedStream> from <music21.stream.Stream OriginalStream> via 'manual'> >>> d1.origin is s2 True >>> d1.client is s1 True >>> import copy >>> d2 = copy.deepcopy(d1) >>> d2.origin is s2 True >>> d1.method = 'measure' >>> d1.method 'measure' Deleting the origin stream does not change the Derivation, since origin is held by strong ref: >>> import gc # Garbage collection >>> del s2 >>> unused = gc.collect() # ensure Garbage collection is run >>> d1 <Derivation of <music21.stream.Stream DerivedStream> from <music21.stream.Stream OriginalStream> via 'measure'> But deleting the client stream changes the Derivation, since client is held by weak ref, and will also delete the origin (so long as client was ever set). >>> del s1 >>> unused = gc.collect() # ensure Garbage collection is run >>> d1 <Derivation of None from None via 'measure'> ''' # CLASS VARIABLES # __slots__ = ( '_client', '_clientId', '_method', '_origin', '_originId', ) # INITIALIZER # def __init__(self, client: base.Music21Object|None = None): # store a reference to the Music21Object that has this Derivation object as a property self._client: weakref.ReferenceType|None = None self._clientId: int|None = None # store python-id to optimize w/o unwrapping self._method: str|None = None # origin could be stored as a weak ref -- the place where the client was derived from. self._origin: base.Music21Object|None = None self._originId: int|None = None # store id to optimize w/o unwrapping # set client; can handle None self.client = client # SPECIAL METHODS # def __deepcopy__(self, memo=None): ''' Manage deepcopying by creating a new reference to the same object. If the origin no longer exists, then origin is set to None. ''' new = type(self)() new.client = self.client new.origin = self.origin return new def __repr__(self): ''' Representation of the Derivation. ''' klass = self.__class__.__name__ via = f' via {self.method!r}' if self.method else '' return f'<{klass} of {self.client} from {self.origin}{via}>' def __getstate__(self): # unwrap weakref for pickling self._client = common.unwrapWeakref(self._client) return SlottedObjectMixin.__getstate__(self) def __setstate__(self, state): SlottedObjectMixin.__setstate__(self, state) self._client = common.wrapWeakref(self._client) # PUBLIC METHODS # # PUBLIC PROPERTIES # @property def client(self) -> base.Music21Object|None: c = common.unwrapWeakref(self._client) if c is None and self._clientId is not None: self._clientId = None self._client = None self._origin = None self._originId = None return c @client.setter def client(self, client: base.Music21Object|None): # client is the Stream that this derivation lives on if client is None: self._clientId = None self._client = None else: self._clientId = id(client) self._client = common.wrapWeakref(client) # type: ignore
[docs] def chain(self) -> Generator[base.Music21Object, None, None]: ''' Iterator/Generator Yields the Streams which this Derivation's client Stream was derived from. This provides a way to obtain all Streams that the client passed through, such as those created by :meth:`~music21.stream.Stream.getElementsByClass` or :meth:`~music21.stream.Stream.flatten`. >>> s1 = stream.Stream() >>> s1.id = 's1' >>> s1.repeatAppend(note.Note(), 10) >>> s1.repeatAppend(note.Rest(), 10) >>> s2 = s1.notesAndRests.stream() >>> s2.id = 's2' >>> s3 = s2.getElementsByClass(note.Note).stream() >>> s3.id = 's3' >>> for y in s3.derivation.chain(): ... print(y) <music21.stream.Stream s2> <music21.stream.Stream s1> >>> list(s3.derivation.chain()) == [s2, s1] True ''' orig: base.Music21Object | None = self.origin while orig is not None: yield orig orig = orig.derivation.origin # pylint: disable=no-member
@property def method(self) -> str|None: ''' Returns or sets the string of the method that was used to generate this Stream. >>> s = stream.Stream() >>> s.derivation.method is None True >>> sNotes = s.notes.stream() >>> sNotes.derivation.method 'notes' Some examples are 'getElementsByClass' etc. >>> s = stream.Stream() >>> s.id = 'lonelyStream' >>> s.append(clef.TrebleClef()) >>> s.append(note.Note()) >>> sNotes = s.notes.stream() >>> sNotes.derivation <Derivation of <music21.stream.Stream lonelyStream> from <music21.stream.Stream lonelyStream> via 'notes'> >>> derived = sNotes.derivation >>> derived.method 'notes' >>> derived.method = 'blah' >>> derived.method 'blah' >>> derived is sNotes.derivation True >>> sNotes.derivation.method 'blah' ''' return self._method @method.setter def method(self, method: str|None): self._method = method @property def origin(self) -> base.Music21Object|None: return self._origin @origin.setter def origin(self, origin: base.Music21Object|None): # for now, origin is not a weak ref if origin is None: self._originId = None self._origin = None else: self._originId = id(origin) self._origin = origin # self._origin = common.wrapWeakref(origin) @property def originId(self) -> int|None: ''' Return the Python id (=memory location) of the origin. (Same as id(derivation.origin). Not the same as derivation.origin.ind) ''' return self._originId @property def rootDerivation(self) -> base.Music21Object|None: r''' Return a reference to the oldest source of this Stream; that is, chain calls to :attr:`~music21.stream.Stream.derivesFrom` until we get to a Stream that cannot be further derived. >>> s1 = stream.Stream() >>> s1.repeatAppend(note.Note(), 10) >>> s1.repeatAppend(note.Rest(), 10) >>> s2 = s1.notesAndRests.stream() >>> s3 = s2.getElementsByClass(note.Note).stream() >>> s3.derivation.rootDerivation is s1 True ''' derivationChain = list(self.chain()) if derivationChain: return derivationChain[-1] else: return None
# ----------------------------------------------------------------------------- class Test(unittest.TestCase): pass # ----------------------------------------------------------------------------- # define presented order in documentation _DOC_ORDER = [Derivation] if __name__ == '__main__': # sys.arg test options will be used in mainTest() import music21 music21.mainTest(Test)