• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cuthbertLab / music21 / 28128686343

24 Jun 2026 08:49PM UTC coverage: 93.136% (+0.004%) from 93.132%
28128686343

Pull #1953

github

web-flow
Merge 8a4590d3d into 603fa2449
Pull Request #1953: Typing of "common" directory

133 of 146 new or added lines in 14 files covered. (91.1%)

81815 of 87845 relevant lines covered (93.14%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.71
/music21/common/objects.py
1
# ------------------------------------------------------------------------------
2
# Name:         common/objects.py
3
# Purpose:      Commonly used Objects and Mixins
4
#
5
# Authors:      Michael Scott Asato Cuthbert
6
#               Christopher Ariza
7
#
8
# Copyright:    Copyright © 2009-2015 Michael Scott Asato Cuthbert
9
# License:      BSD, see license.txt
10
# ------------------------------------------------------------------------------
11
from __future__ import annotations
1✔
12

13
__all__ = [
1✔
14
    'defaultlist',
15
    'SingletonCounter',
16
    'RelativeCounter',
17
    'SlottedObjectMixin',
18
    'EqualSlottedObjectMixin',
19
    'FrozenObject',
20
    'Timer',
21
]
22

23
from collections.abc import Callable, Iterator
1✔
24
import collections
1✔
25
import inspect
1✔
26
import time
1✔
27
import typing as t
1✔
28
import weakref
1✔
29

30

31
class RelativeCounter(collections.Counter):
1✔
32
    '''
33
    A counter that iterates from most common to least common
34
    and can return new RelativeCounters that adjust for proportion or percentage.
35

36
    >>> l = ['b', 'b', 'a', 'a', 'a', 'a', 'c', 'd', 'd', 'd'] + ['e'] * 10
37
    >>> rc = common.RelativeCounter(l)
38
    >>> for k in rc:
39
    ...     print(k, rc[k])
40
    e 10
41
    a 4
42
    d 3
43
    b 2
44
    c 1
45

46
    Ties are iterated according to which appeared first in the generated list.
47

48
    >>> rcProportion = rc.asProportion()
49
    >>> rcProportion['b']
50
    0.1
51
    >>> rcProportion['e']
52
    0.5
53
    >>> rcPercentage = rc.asPercentage()
54
    >>> rcPercentage['b']
55
    10.0
56
    >>> rcPercentage['e']
57
    50.0
58

59
    >>> for k, perc in rcPercentage.items():
60
    ...     print(k, perc)
61
    e 50.0
62
    a 20.0
63
    d 15.0
64
    b 10.0
65
    c 5.0
66
    '''
67
    # pylint:disable=abstract-method
68

69
    def __iter__(self) -> Iterator[t.Any]:
1✔
70
        sortedKeys = sorted(super().__iter__(), key=lambda x: self[x], reverse=True)
1✔
71
        yield from sortedKeys
1✔
72

73
    # deliberately a generator (not dict's re-iterable ItemsView) so that the
74
    # pairs follow the most-common-first order of the overridden __iter__.
75
    def items(self) -> Iterator[tuple[t.Any, t.Any]]:  # type: ignore[override]
1✔
76
        for k in self:
1✔
77
            yield k, self[k]
1✔
78

79
    def asProportion(self) -> RelativeCounter:
1✔
80
        selfLen = sum(self[x] for x in self)
1✔
81
        outDict = {}
1✔
82
        for y in self:
1✔
83
            outDict[y] = self[y] / selfLen
1✔
84
        # noinspection PyTypeChecker
85
        new = self.__class__(outDict)
1✔
86
        return new
1✔
87

88
    def asPercentage(self) -> RelativeCounter:
1✔
89
        selfLen = sum(self[x] for x in self)
1✔
90
        outDict = {}
1✔
91
        for y in self:
1✔
92
            outDict[y] = self[y] * 100 / selfLen
1✔
93
        # noinspection PyTypeChecker
94
        new = self.__class__(outDict)
1✔
95
        return new
1✔
96

97

98
class defaultlist(list):
1✔
99
    '''
100
    Call a function for every time something is missing:
101

102
    >>> a = common.defaultlist(lambda:True)
103
    >>> a[5]
104
    True
105
    '''
106
    def __init__(self, fx: Callable[[], t.Any]) -> None:
1✔
107
        super().__init__()
1✔
108
        self._fx = fx
1✔
109

110
    def _fill(self, index: int) -> None:
1✔
111
        while len(self) <= index:
1✔
112
            self.append(self._fx())
1✔
113

114
    def __setitem__(self, index: int, value: t.Any) -> None:  # type: ignore[override]
1✔
115
        self._fill(index)
1✔
116
        list.__setitem__(self, index, value)
1✔
117

118
    def __getitem__(self, index: int) -> t.Any:  # type: ignore[override]
1✔
119
        self._fill(index)
1✔
120
        return list.__getitem__(self, index)
1✔
121

122

123
_singletonCounter = {'value': 0}
1✔
124

125

126
class SingletonCounter:
1✔
127
    '''
128
    A simple counter that can produce unique numbers (in ascending order)
129
    regardless of how many instances exist.
130

131
    Instantiate and then call it.
132

133
    >>> sc = common.SingletonCounter()
134
    >>> v0 = sc()
135
    >>> v1 = sc()
136
    >>> v1 > v0
137
    True
138
    >>> sc2 = common.SingletonCounter()
139
    >>> v2 = sc2()
140
    >>> v2 > v1
141
    True
142
    '''
143
    def __init__(self) -> None:
1✔
144
        pass
1✔
145

146
    def __call__(self) -> int:
1✔
147
        post = _singletonCounter['value']
1✔
148
        _singletonCounter['value'] += 1
1✔
149
        return post
1✔
150

151
# ------------------------------------------------------------------------------
152

153

154
class SlottedObjectMixin:
1✔
155
    r'''
156
    Provides template for classes implementing slots allowing it to be pickled
157
    properly, even if there are weakrefs in the slots, or it is subclassed
158
    by something that does not define slots.
159

160
    Only use SlottedObjectMixins for objects that we expect to make so many of
161
    that memory storage and speed become an issue. Thus, unless you are Xenakis,
162
    Glissdata is probably not the best example:
163

164
    >>> import pickle
165
    >>> class Glissdata(common.SlottedObjectMixin):
166
    ...     __slots__ = ('time', 'frequency')
167
    >>> s = Glissdata()
168
    >>> s.time = 0.125
169
    >>> s.frequency = 440.0
170
    >>> #_DOCS_SHOW out = pickle.dumps(s)
171
    >>> #_DOCS_SHOW pickleLoad = pickle.loads(out)
172
    >>> pickleLoad = s #_DOCS_HIDE -- cannot define classes for pickling in doctests
173
    >>> pickleLoad.time, pickleLoad.frequency
174
    (0.125, 440.0)
175

176
    OMIT_FROM_DOCS
177

178
    >>> class BadSubclass(Glissdata):
179
    ...     pass
180

181
    >>> bsc = BadSubclass()
182
    >>> bsc.amplitude = 2
183
    >>> #_DOCS_SHOW out = pickle.dumps(bsc)
184
    >>> #_DOCS_SHOW outLoad = pickle.loads(out)
185
    >>> outLoad = bsc #_DOCS_HIDE -- cannot define classes for pickling in doctests
186
    >>> outLoad.amplitude
187
    2
188

189
    This is in OMIT
190
    '''
191

192
    # CLASS VARIABLES #
193

194
    __slots__: tuple[str, ...] = ()
1✔
195

196
    # SPECIAL METHODS #
197

198
    def __getstate__(self) -> dict[str, t.Any]:
1✔
199
        if getattr(self, '__dict__', None) is not None:
1✔
200
            state = self.__dict__.copy()
1✔
201
        else:
202
            state = {}
1✔
203
        slots = self._getSlotsRecursive()
1✔
204
        for slot in slots:
1✔
205
            sValue = getattr(self, slot, None)
1✔
206
            if isinstance(sValue, weakref.ReferenceType):
1✔
207
                sValue = sValue()
×
208
                print(f'Warning: uncaught weakref found in {self!r} - {slot}, '
×
209
                      + 'will not be wrapped again')
210
            state[slot] = sValue
1✔
211
        return state
1✔
212

213
    def __setstate__(self, state: dict[str, t.Any]) -> None:
1✔
214
        for slot, value in state.items():
1✔
215
            setattr(self, slot, value)
1✔
216

217
    def _getSlotsRecursive(self) -> set[str]:
1✔
218
        '''
219
        Find all slots recursively.
220

221
        A private attribute so as not to change the contents of inheriting
222
        objects private interfaces:
223

224
        >>> b = beam.Beam()
225
        >>> sSet = b._getSlotsRecursive()
226

227
        sSet is a set -- independent order.  Thus, for the doctest
228
        we need to preserve the order:
229

230
        >>> sorted(list(sSet))
231
        ['_editorial', '_style', 'direction', 'id', 'independentAngle', 'number', 'type']
232

233
        When a normal Beam won't cut it:
234

235
        >>> class FunkyBeam(beam.Beam):
236
        ...     __slots__ = ('funkiness', 'groovability')
237

238
        >>> fb = FunkyBeam()
239
        >>> sSet = fb._getSlotsRecursive()
240
        >>> sorted(list(sSet))
241
        ['_editorial', '_style', 'direction', 'funkiness', 'groovability',
242
            'id', 'independentAngle', 'number', 'type']
243
        '''
244
        slots: set[str] = set()
1✔
245
        for cls in self.__class__.mro():
1✔
246
            slots.update(getattr(cls, '__slots__', ()))
1✔
247
        return slots
1✔
248

249

250
class EqualSlottedObjectMixin(SlottedObjectMixin):
1✔
251
    '''
252
    Same as above, but __eq__ and __ne__ functions are defined based on the slots.
253

254
    Slots are the only things compared, so do not mix with a __dict__ based object.
255

256
    The equal comparison ignores differences in .id
257
    '''
258
    __slots__: tuple[str, ...] = ()
1✔
259

260
    def __eq__(self, other: object) -> bool:
1✔
261
        if type(self) is not type(other):
1✔
262
            return False
×
263
        for thisSlot in self._getSlotsRecursive():
1✔
264
            if thisSlot == 'id':
1✔
265
                continue
1✔
266
            if getattr(self, thisSlot) != getattr(other, thisSlot):
1✔
267
                return False
1✔
268
        return True
1✔
269

270
    def __ne__(self, other: object) -> bool:
1✔
271
        '''
272
        Defining __ne__ explicitly so that it inherits the same as __eq__
273
        '''
274
        return not (self == other)
1✔
275

276

277
# my own version which Ned Batchelder (as usual) already anticipated:
278
# https://stackoverflow.com/questions/4828080/how-to-make-an-immutable-object-in-python
279
class FrozenObject(EqualSlottedObjectMixin):
1✔
280
    __slots__: tuple[str, ...] = ()
1✔
281

282
    def _check_init(self, key: str|None = None) -> bool:
1✔
283
        if key == '__class__':
1✔
284
            return True
×
285
        if not getattr(self, 'frozen', True):
1✔
286
            return True
×
287

288
        for st in inspect.stack():
1✔
289
            if (st.frame.f_code.co_name in ('__init__', '__new__', '__setstate__')
1✔
290
                    and 'self' in st.frame.f_locals
291
                    and st.frame.f_locals['self'].__class__ == self.__class__):
292
                return True
1✔
293
        raise TypeError(f'This {self.__class__.__name__} instance is immutable.')
1✔
294

295
    def __setattr__(self, key: str, value: t.Any) -> None:
1✔
296
        self._check_init(key)
1✔
297
        super().__setattr__(key, value)
1✔
298

299
    def __delattr__(self, key: str) -> None:
1✔
300
        self._check_init(key)
×
301
        super().__delattr__(key)
×
302

303
    def __setitem__(self, key: t.Any, value: t.Any) -> None:
1✔
304
        if hasattr(super(), '__setitem__'):
×
305
            self._check_init()
×
NEW
306
            super().__setitem__(key, value)  # type: ignore[misc]
×
307
        raise TypeError(f'{self.__class__} object is not subscriptable')
×
308

309
    def __delitem__(self, key: t.Any) -> None:
1✔
310
        if hasattr(super(), '__delitem__'):
×
311
            self._check_init()
×
NEW
312
            super().__delitem__(key)  # type: ignore[misc]
×
313
        raise TypeError(f'{self.__class__} object is not subscriptable')
×
314

315
    def __hash__(self) -> int:
1✔
316
        out = []
1✔
317
        for s in self._getSlotsRecursive():
1✔
318
            out.append((s, getattr(self, s)))
1✔
319
        return hash(tuple(out))
1✔
320

321

322
# ------------------------------------------------------------------------------
323
class Timer:
1✔
324
    '''
325
    An object for timing. Call it to get the current time since starting.
326

327
    >>> timer = common.Timer()
328
    >>> now = timer()
329
    >>> import time  #_DOCS_HIDE
330
    >>> time.sleep(0.01)  #_DOCS_HIDE  -- some systems are extremely fast or have wide deltas
331
    >>> nowNow = timer()
332
    >>> nowNow > now
333
    True
334

335
    Call `stop` to stop it. Calling `start` again will reset the number
336

337
    >>> timer.stop()
338
    >>> stopTime = timer()
339
    >>> stopNow = timer()
340
    >>> stopTime == stopNow
341
    True
342

343
    All this had better take less than one second!
344

345
    >>> stopTime < 1
346
    True
347
    '''
348
    def __init__(self) -> None:
1✔
349
        # start on init
350
        self._tStart: float = time.time()
1✔
351
        self._tDif: float = 0
1✔
352
        self._tStop: float|None = None
1✔
353

354
    def start(self) -> None:
1✔
355
        '''
356
        Explicit start method; will clear previous values.
357

358
        Start always happens on initialization.
359
        '''
360
        self._tStart = time.time()
1✔
361
        self._tStop = None  # show that a new run has started so __call__ works
1✔
362
        self._tDif = 0
1✔
363

364
    def stop(self) -> None:
1✔
365
        self._tStop = time.time()
1✔
366
        self._tDif = self._tStop - self._tStart
1✔
367

368
    def clear(self) -> None:
1✔
NEW
369
        self._tStart = time.time()
×
370
        self._tStop = None
×
371
        self._tDif = 0
×
372

373
    def __call__(self) -> float:
1✔
374
        '''
375
        Reports current time or, if stopped, stopped time.
376
        '''
377
        # if stopped, gets _tDif; if not stopped, gets current time
378
        if self._tStop is None:  # if not stopped yet
1✔
379
            timeDifference = time.time() - self._tStart
1✔
380
        else:
381
            timeDifference = self._tDif
1✔
382
        return timeDifference
1✔
383

384
    def __str__(self) -> str:
1✔
385
        if self._tStop is None:  # if not stopped yet
1✔
386
            timeDifference = time.time() - self._tStart
1✔
387
        else:
388
            timeDifference = self._tDif
×
389
        return str(round(timeDifference, 3))
1✔
390

391

392
if __name__ == '__main__':
393
    import music21
394
    music21.mainTest()
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc