-
Notifications
You must be signed in to change notification settings - Fork 402
/
derivation.py
351 lines (285 loc) · 10.2 KB
/
derivation.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Name: derivation.py
# Purpose: Class for storing and managing Stream-based derivations
#
# Authors: Christopher Ariza
# Josiah Oberholtzer
# Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2011-2014 Michael Scott Asato Cuthbert and the music21
# Project
# 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')
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
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 should be stored as a weak ref -- the place where the client was derived from.
self._origin = 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, than 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
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)