# ------------------------------------------------------------------------------
# Name: prebase.py
# Purpose: classes for anything in music21 to inherit from.
#
# Authors: Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2019 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
Classes for pseudo-m21 objects to inherit from. The most important attributes that nearly
everything in music21 -- not just things that live in streams --
should inherit from are given below.
Concept borrowed from m21j.
'''
from __future__ import annotations
import typing as t
import unittest
[docs]
class ProtoM21Object:
'''
A class for pseudo-m21 objects to inherit from. Any object can inherit from
ProtoM21Object, and it makes sense for anything a user is likely to encounter
to inherit from it. Certain translators, etc. can choose to skip it.
>>> class PitchCounter(prebase.ProtoM21Object):
... def _reprInternal(self):
... return 'no pitches'
>>> pc = PitchCounter()
>>> pc.classes
('PitchCounter', 'ProtoM21Object', 'object')
>>> PitchCounter in pc.classSet
True
>>> 'Note' in pc.classSet
False
For a True/False intersection check against an iterable, use `classSet.isdisjoint`:
>>> classList = ('music21.note.Note', 'music21.note.Rest')
>>> pc.classSet.isdisjoint(classList)
True
>>> repr(pc)
'<...PitchCounter no pitches>'
ProtoM21Objects, like other Python primitives, cannot be put into streams --
this is what base.Music21Object does.
A ProtoM21Object defines several methods relating to unified representation
and keeping track of the classes of the object. It has no instance attributes
or properties, and thus adds a very small creation time impact: recent
tests show that an empty object with an empty `__init__()` method can
be created in about 175ns while an empty object that subclasses ProtoM21Object
with the same empty `__init__()` takes only 180ns, or a 5ns impact. On
real objects, the creation time percentage hit is usually much smaller.
ProtoM21Objects have no __init__() defined, so do not call super().__init__() on
objects that only inherit from ProtoM21Object unless you like wasting 200ns.
'''
# Defines the order of presenting names in the documentation; use strings
_DOC_ORDER = [
'classes',
'classSet',
]
# documentation for all attributes (not properties or methods)
_DOC_ATTR: dict[str, str] = {}
# this dictionary stores as a tuple of strings for each Class so that
# it only needs to be made once (11 microseconds per call, can be
# a big part of iteration; from cache just 1 microsecond)
_classTupleCacheDict: dict[type, tuple[str, ...]] = {}
_classSetCacheDict: dict[type, frozenset[str|type]] = {}
__slots__: tuple[str, ...] = ()
@property
def classes(self) -> tuple[str, ...]:
'''
Returns a tuple containing the names (strings, not objects) of classes that this
object belongs to -- starting with the object's class name and going up the mro()
for the object.
Notes are Music21Objects:
>>> n = note.Note('C#')
>>> n.classes
('Note', 'NotRest', 'GeneralNote', 'Music21Object', 'ProtoM21Object', 'object')
Durations are not, but they inherit from ProtoM21Object.
>>> d = duration.Duration('half')
>>> d.classes
('Duration', 'ProtoM21Object', 'SlottedObjectMixin', 'object')
Having quick access to these things as strings makes it easier to do comparisons:
Example: find GClefs that are not Treble clefs (or treble 8vb, etc.):
>>> s = stream.Stream()
>>> s.insert(10, clef.GClef())
>>> s.insert(20, clef.TrebleClef())
>>> s.insert(30, clef.FrenchViolinClef())
>>> s.insert(40, clef.Treble8vbClef())
>>> s.insert(50, clef.BassClef())
>>> s2 = stream.Stream()
>>> for thing in s:
... if isinstance(thing, clef.GClef) and not isinstance(thing, clef.TrebleClef):
... s2.insert(thing)
>>> s2.show('text')
{10.0} <music21.clef.GClef>
{30.0} <music21.clef.FrenchViolinClef>
* Changed in v2: returns a tuple, not a list.
'''
try:
return self._classTupleCacheDict[self.__class__]
except KeyError:
classTuple = tuple(x.__name__ for x in self.__class__.mro())
self._classTupleCacheDict[self.__class__] = classTuple
return classTuple
@property
def classSet(self) -> frozenset[str|type]:
'''
Returns a set (that is, unordered, but indexed) of all classes that
this class belongs to, including
string names, fullyQualified string names, and objects themselves.
It's cached on a per-class basis, so makes for a really fast way of checking to
see if something belongs
to a particular class when you don't know if the user has given a string,
a fully qualified string name, or an object.
Did I mention it's fast? It's not as fast as x in n.classes or isinstance(n, x)
if you know whether it's a string or class, but this is good and safe to take
in either a string or a class.
>>> n = note.Note()
>>> 'Note' in n.classSet
True
>>> 'music21.note.Note' in n.classSet
True
>>> note.Note in n.classSet
True
>>> 'Rest' in n.classSet
False
>>> note.Rest in n.classSet
False
For checking if an object is part of any number of objects use `not`
with `isdisjoint`. A little unwieldy but works super fast:
>>> checkClasses = (spanner.Slur, note.NotRest)
>>> not n.classSet.isdisjoint(checkClasses)
True
>>> object in n.classSet
True
>>> sorted([s for s in n.classSet if isinstance(s, str)])
['GeneralNote',
'Music21Object',
'NotRest',
'Note',
'ProtoM21Object',
'base.Music21Object',
'builtins.object',
'music21.base.Music21Object',
'music21.note.GeneralNote',
'music21.note.NotRest',
'music21.note.Note',
'music21.prebase.ProtoM21Object',
'note.GeneralNote',
'note.NotRest',
'note.Note',
'object',
'prebase.ProtoM21Object']
>>> sorted([s for s in n.classSet if not isinstance(s, str)], key=lambda x: x.__name__)
[<class 'music21.note.GeneralNote'>,
<class 'music21.base.Music21Object'>,
<class 'music21.note.NotRest'>,
<class 'music21.note.Note'>,
<class 'music21.prebase.ProtoM21Object'>,
<class 'object'>]
* Changed in v8: partially qualified objects such as 'note.Note' have been added.
'''
try:
return self._classSetCacheDict[self.__class__]
except KeyError:
classList: list[str|type] = list(self.classes)
classList.extend(self.__class__.mro())
fullyQualifiedStrings = [x.__module__ + '.' + x.__name__ for x in self.__class__.mro()]
classList.extend(fullyQualifiedStrings)
partiallyQualifiedStrings = [x.replace('music21.', '') for x in fullyQualifiedStrings]
classList.extend(partiallyQualifiedStrings)
classSet = frozenset(classList)
self._classSetCacheDict[self.__class__] = classSet
return classSet
def __repr__(self) -> str:
'''
Defines the default representation for a ProtoM21Object
which includes the module name, the class name, and additional
information, such as the memory location:
>>> p = prebase.ProtoM21Object()
>>> repr(p)
'<music21.prebase.ProtoM21Object object at 0x112590380>'
The additional information is defined in the `_reprInternal` method,
so objects inheriting from ProtoM21Object (such as Music21Object)
should change `_reprInternal` and not `__repr__`.
Except for music21.base itself, any object in a file called base.py
has the base part removed.
>>> from music21.midi.base import MidiEvent
>>> me = MidiEvent()
>>> repr(me)
'<music21.midi.MidiEvent UNKNOWN, track=None>'
>>> me._reprInternal()
'UNKNOWN, track=None'
'''
reprHead = '<'
if self.__module__ != '__main__':
reprHead += self.__module__ + '.'
reprHead += self.__class__.__qualname__
if '.base.' in reprHead and 'music21.base' not in reprHead:
reprHead = reprHead.replace('.base', '')
strRepr = self._reprInternal()
if strRepr and not strRepr.startswith(':'):
reprHead += ' '
if strRepr:
reprHead += strRepr.strip()
return reprHead + '>'
def _reprInternal(self) -> str:
'''
Defines the insides of the representation.
Overload this method for most objects.
A default representation:
>>> p = prebase.ProtoM21Object()
>>> p._reprInternal()
'object at 0x112590380'
A more complex `_reprInternal` that handles the case of objects
with `.id` defined is found in Music21Object.
'''
return f'object at {hex(id(self))}'
del t
class Test(unittest.TestCase):
def testCopyAndDeepcopy(self):
from music21.test.commonTest import testCopyAll
testCopyAll(self, globals())
def test_reprInternal(self):
from music21.base import Music21Object
b = Music21Object()
b.id = 'hello'
r = repr(b)
self.assertEqual(r, '<music21.base.Music21Object id=hello>')
# ---------------------------------------------------------
if __name__ == '__main__':
import music21
music21.mainTest(Test)