Source code for music21.metadata.primitives

# -----------------------------------------------------------------------------
# Name:         primitives.py
# Purpose:      music21 classes for representing score and work metadata
#
# Authors:      Christopher Ariza
#               Michael Scott Asato Cuthbert
#               Greg Chapman
#
# Copyright:    Copyright © 2010-22 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# -----------------------------------------------------------------------------
from __future__ import annotations

__all__ = [
    'Contributor',
    'Copyright',
    'Creator',
    'Date',
    'DateBetween',
    'DatePrimitive',
    'DateRelative',
    'DateSelection',
    'DateSingle',
    'Imprint',
    'Text',
    'ValueType',
]

from collections.abc import Iterable
import datetime
import typing as t
import unittest

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

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

environLocal = environment.Environment('metadata.primitives')



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

[docs] class Date(prebase.ProtoM21Object): r''' A single date value, specified by year, month, day, hour, minute, and second. Note that this class has been created, instead of using Python's datetime, to provide greater flexibility for processing unconventional dates, ancient dates, dates with error, and date ranges. The :attr:`~music21.metadata.Date.datetime` property can be used to retrieve a datetime object when necessary. Additionally, each value can be specified as `uncertain` or `approximate`; if None, assumed to be certain. Date objects are fundamental components of :class:`~music21.metadata.DateSingle` and related subclasses that represent single dates and date ranges. >>> a = metadata.Date(year=1843, yearError='approximate') >>> a.year 1843 >>> a.yearError 'approximate' >>> a = metadata.Date(year='1843?') >>> a.yearError 'uncertain' >>> d = metadata.Date(year=1805, month=3, monthError='approximate') >>> str(d) '1805/03~/--' Note that milliseconds are not retained, as this is a tool for musicology and not for file timestamps. However, unlike datetime objects, dates in the distant past are supported (though not currently BC/BCE dates). ''' # CLASS VARIABLES # approximateSymbols = ('~', 'x') uncertainSymbols = ('?', 'z') priorTimeSymbols = ('<', '{', '>', '}') # INITIALIZER # def __init__(self, *, year: int|str|None = None, month: int|str|None = None, day: int|str|None = None, hour: int|str|None = None, minute: int|str|None = None, second: int|float|str|None = None, yearError: str|None = None, monthError: str|None = None, dayError: str|None = None, hourError: str|None = None, minuteError: str|None = None, secondError: str|None = None): if year is not None and yearError is None: year, yearError = self._stripError(year) if month is not None and monthError is None: month, monthError = self._stripError(month) if day is not None and dayError is None: day, dayError = self._stripError(day) if hour is not None and hourError is None: hour, hourError = self._stripError(hour) if minute is not None and minuteError is None: minute, minuteError = self._stripError(minute) if second is not None and secondError is None: second, secondError = self._stripError(second) self._sanityCheck(year=year, month=month, day=day, hour=hour, minute=minute, second=second) self.year = t.cast(int|None, year) self.month = t.cast(int|None, month) self.day = t.cast(int|None, day) self.hour = t.cast(int|None, hour) self.minute = t.cast(int|None, minute) self.second = t.cast(int|None, second) # error: can be 'approximate', 'uncertain' or None. # None is assumed to be certain self.yearError: str|None = yearError self.monthError: str|None = monthError self.dayError: str|None = dayError self.hourError: str|None = hourError self.minuteError: str|None = minuteError self.secondError: str|None = secondError self.attrNames = ('year', 'month', 'day', 'hour', 'minute', 'second') # SPECIAL METHODS #
[docs] def __eq__(self, other) -> bool: return str(self) == str(other)
def __str__(self): r''' Return a string representation, including error if defined. >>> d = metadata.Date() >>> d.loadStr('1030?/12~/?4') >>> str(d) '1030?/12~/04?' ''' # Note, cannot use datetime.strftime('%Y.%m.%d') even if all are not-None, # as it does not support dates lower than 1900! msg = [] if self.hour is None and self.minute is None and self.second is None: breakIndex = 3 # index else: breakIndex = 7 for i, attr in enumerate(self.attrNames): if i >= breakIndex: break value = t.cast(str, getattr(self, attr)) error = t.cast(str, getattr(self, attr + 'Error')) if value is None: msg.append('--') else: if attr == 'year': fmt = '%04.i' else: fmt = '%02.i' sub = fmt % value if error is not None: sub += Date.errorToSymbol(error) sub = str(sub) msg.append(sub) out = '/'.join(msg[:4]) if len(msg) > 4: out += ':' + ':'.join(msg[4:]) return out # PRIVATE METHODS # def _stripError(self, value: int|float|str, ) -> tuple[int, str|None]: r''' Strip error symbols from a numerical value. Return cleaned source and sym. Only one error symbol is expected per string. >>> d = metadata.Date() >>> d._stripError('1247~') (1247, 'approximate') >>> d._stripError('63?') (63, 'uncertain') Milliseconds are not retained -- this is for musicology, not computers: >>> d._stripError('4.43') (4, None) ''' uncertainty: str|None = None if isinstance(value, str): # if a number, let pass sym = self.approximateSymbols + self.uncertainSymbols + self.priorTimeSymbols found = None for char in value: if char in sym: found = char break if found in self.approximateSymbols: value = value.replace(found, '') uncertainty = 'approximate' elif found in self.uncertainSymbols: value = value.replace(found, '') uncertainty = 'uncertain' elif found in self.priorTimeSymbols: value = value.replace(found, '') uncertainty = 'priority' # cannot convert string '4.43' directly to int value = float(value) return int(value), uncertainty def _sanityCheck(self, *, year, month, day, hour, minute, second): def month_fail(m, d, y): # not checking Gregorian leap year viability, as it changes historically. return ((month in (4, 6, 9, 11) and day == 31) or (month == 2 and day > 29) or (month == 2 and day == 29 and year is not None and year % 4)) if month is not None and (month < 1 or month > 12): raise ValueError(f'Month must be between 1 and 12, not {month}.') if day is not None and ( day > 31 or day < 1 or month is not None and month_fail(month, day, year)): raise ValueError(f'Day {day} is not possible with month {month}.') if hour is not None and (hour < 0 or hour > 23): raise ValueError('Hour must be between 0 and 23') if minute is not None and (minute < 0 or minute > 59): raise ValueError('Minute must be between 0 and 59') if second is not None and (second < 0 or second > 59): raise ValueError('Second must be between 0 and 59') # PUBLIC METHODS #
[docs] @staticmethod def errorToSymbol(value): r''' Convert an error string (approximate, uncertain) into a symbol. >>> metadata.Date.errorToSymbol('approximate') '~' >>> metadata.Date.errorToSymbol('uncertain') '?' ''' if value.lower() in Date.approximateSymbols + ('approximate',): return Date.approximateSymbols[0] if value.lower() in Date.uncertainSymbols + ('uncertain',): return Date.uncertainSymbols[0]
[docs] def load(self, value: DateParseType): r''' Load values by string, datetime object, or Date object: >>> a = metadata.Date(year=1843, month=3, day=3) >>> b = metadata.Date() >>> b.load(a) >>> b.year 1843 If there is an error, a ValueError is raised, but the incorrect values are retained: >>> d = metadata.Date() >>> d.load('1999/14/32/25:60:61') Traceback (most recent call last): ValueError: Month must be between 1 and 12, not 14. >>> str(d) '1999/14/32/25:60:61' ''' if isinstance(value, datetime.datetime): self.loadDatetime(value) elif isinstance(value, str): self.loadStr(value) elif isinstance(value, int): self.loadStr(str(value)) elif isinstance(value, Date): self.loadOther(value) else: raise exceptions21.MetadataException(f'Cannot load data: {value}') self._sanityCheck(year=self.year, month=self.month, day=self.day, hour=self.hour, minute=self.minute, second=self.second)
[docs] def loadDatetime(self, dt: datetime.datetime) -> None: # noinspection PyShadowingNames r''' Load time data from a datetime object: >>> import datetime >>> dt = datetime.datetime(2005, 2, 1) >>> dt datetime.datetime(2005, 2, 1, 0, 0) >>> m21mdDate = metadata.Date() >>> m21mdDate.loadDatetime(dt) >>> str(m21mdDate) '2005/02/01' ''' for attr in self.attrNames: if hasattr(dt, attr): # names here are the same, so we can directly map value = getattr(dt, attr) if value not in (0, None): setattr(self, attr, value)
[docs] def loadOther(self, other: Date) -> None: r''' Load values based on another Date object. (the "Other" in "loadOther" means another Date object, not just anything. >>> a = metadata.Date(year=1843, month=3, day=3, yearError='approximate') >>> b = metadata.Date() >>> b.loadOther(a) >>> b.year 1843 >>> b.yearError 'approximate' ''' for attr in self.attrNames: if getattr(other, attr) is not None: setattr(self, attr, getattr(other, attr)) errorAttr = attr + 'Error' if getattr(other, errorAttr) is not None: setattr(self, errorAttr, getattr(other, errorAttr))
[docs] def loadStr(self, dateStr: str) -> None: r''' Load a string date representation, which might have approximate symbols. Assume `year/month/day/hour:minute:second`: >>> d = metadata.Date() >>> d.loadStr('1030?/12~/?4') >>> d.month, d.monthError (12, 'approximate') >>> d.year, d.yearError (1030, 'uncertain') >>> d.month, d.monthError (12, 'approximate') >>> d.day, d.dayError (4, 'uncertain') >>> d = metadata.Date() >>> d.loadStr('1834/12/4/4:50:32') >>> d.minute, d.second (50, 32) ''' post: list[int|None] = [] postError: list[str|None] = [] dateStr = dateStr.replace(':', '/') dateStr = dateStr.replace(' ', '') for chunk in dateStr.split('/'): if chunk == '--': value, error = None, None else: value, error = self._stripError(chunk) post.append(value) postError.append(error) # assume in order in post list for i in range(len(self.attrNames)): if len(post) > i: # only assign for those specified setattr(self, self.attrNames[i], post[i]) if postError[i] is not None: setattr(self, self.attrNames[i] + 'Error', postError[i])
# PUBLIC PROPERTIES # @property def datetime(self): r''' Get a datetime object from a metadata.Date() object >>> a = metadata.Date(year=1843, month=3, day=3) >>> str(a) '1843/03/03' >>> a.datetime datetime.datetime(1843, 3, 3, 0, 0) Lack of a required date element raises an exception: >>> a = metadata.Date(year=1843, month=3) >>> str(a) '1843/03/--' >>> a.datetime Traceback (most recent call last): TypeError: function missing required argument 'day' (pos 3) ''' post = [] # order here is order for datetime # TODO: need defaults for incomplete times. for attr in self.attrNames: # need to be integers value = getattr(self, attr) if value is None: break post.append(int(value)) return datetime.datetime(*post) # pylint: disable=no-value-for-parameter @property def hasTime(self): r''' Return True if any time elements are defined: >>> a = metadata.Date(year=1843, month=3, day=3) >>> a.hasTime False >>> b = metadata.Date(year=1843, month=3, day=3, minute=3) >>> b.hasTime True ''' if (self.hour is not None or self.minute is not None or self.second is not None): return True else: return False @property def hasError(self): r''' Return True if any data points have error defined: >>> a = metadata.Date( ... year=1843, ... month=3, ... day=3, ... dayError='approximate', ... ) >>> a.hasError True >>> b = metadata.Date( ... year=1843, ... month=3, ... day=3, ... minute=3, ... ) >>> b.hasError False ''' for attr in self.attrNames: if getattr(self, attr + 'Error') is not None: return True return False
# -----------------------------------------------------------------------------
[docs] class DatePrimitive(prebase.ProtoM21Object): ''' A default class for all date objects, which can have different types and different "relevance" values. Note that the interaction between uncertainty on an entire DatePrimitive object vs uncertainty on a particular Date value, like month, is ill-defined and needs work. ''' # INITIALIZER # def __init__(self, relevance: str = 'certain'): self._data: list[Date] = [] self._relevance = '' # managed by property # not yet implemented # store an array of values marking if date data itself # is certain, approximate, or uncertain # here, dataError is relevance self._dataUncertainty: list[str|None] = [] self.relevance = relevance # will use property # SPECIAL METHODS #
[docs] def __eq__(self, other) -> bool: ''' >>> dd = metadata.DateSingle('1805/3/12', 'uncertain') >>> dd2 = metadata.DateSingle('1805/3/12', 'uncertain') >>> str(dd) '1805/03/12' >>> dd == dd2 True >>> dd2.relevance='certain' >>> dd == dd2 False ''' return (type(self) is type(other) and self._data == other._data and self._dataUncertainty == other._dataUncertainty and self.relevance == other.relevance)
def _reprInternal(self) -> str: return str(self) def __str__(self): if len(self._data) == 0: return '' elif len(self._data) == 1: return str(self._data[0]) else: return str([str(d) for d in self._data]) # PUBLIC PROPERTIES # @property def datetime(self): r''' Get a datetime object. >>> a = metadata.DateSingle('1843/03/03') >>> str(a) '1843/03/03' >>> a.datetime datetime.datetime(1843, 3, 3, 0, 0) >>> a = metadata.DateSingle('1843/03') >>> str(a) '1843/03/--' ''' # get from stored Date object return self._data[0].datetime @property def relevance(self): ''' The relevance attribute takes one of three values, `'certain'`, `'approximate'`, or `'uncertain'`. ''' return self._relevance @relevance.setter def relevance(self, value): if value in ('certain', 'approximate', 'uncertain'): self._relevance = value self._dataUncertainty = [] # only here is dataError the same as relevance self._dataUncertainty.append(value) else: raise exceptions21.MetadataException( f'Relevance value is not supported by this object: {value!r}')
[docs] class DateSingle(DatePrimitive): r''' Store a date, either as certain, approximate, or uncertain relevance. The relevance attribute is limited within each DateSingle subclass depending on the design of the class. Alternative relevance types should be configured as other DateSingle subclasses. >>> dd = metadata.DateSingle('2009/12/31', 'approximate') >>> dd <music21.metadata.primitives.DateSingle 2009/12/31> >>> str(dd) '2009/12/31' >>> dd.relevance 'approximate' >>> dd = metadata.DateSingle('1805/3/12', 'uncertain') >>> str(dd) '1805/03/12' ''' def __init__(self, data: DateParseType = '', relevance='certain'): super().__init__(relevance) self._prepareData(data) def __str__(self): return str(self._data[0]) def _prepareData(self, data: DateParseType): r''' Assume a string is supplied as argument ''' # here, using a list to store one object; this provides more # compatibility w/ other formats self._data = [] # clear list self._dataUncertainty = [self.relevance] self._data.append(Date()) self._data[0].load(data)
# -----------------------------------------------------------------------------
[docs] class DateRelative(DatePrimitive): r''' Store a relative date, sometime `prior` or sometime `after`, `onorbefore`, or onorafter`. >>> dd = metadata.DateRelative('2009/12/31', 'prior') >>> str(dd) 'prior to 2009/12/31' >>> dd.relevance = 'after' >>> str(dd) 'after 2009/12/31' >>> dd = metadata.DateRelative('2009/12/31', 'certain') Traceback (most recent call last): music21.exceptions21.MetadataException: Relevance value is not supported by this object: 'certain' ''' # INITIALIZER # def __init__(self, data: DateParseType = '', relevance='after'): # not a useless constructor because default value for relevance changed super().__init__(relevance) self._prepareData(data) # PUBLIC PROPERTIES # def __str__(self): r = self.relevance ds = super().__str__() if r == 'prior': return 'prior to ' + ds elif r == 'onorbefore': return ds + ' or earlier' elif r == 'onorafter': return ds + ' or later' else: return 'after ' + ds @property def relevance(self): ''' The relevance attribute takes one of four values, `'prior'`, `'after'`, or `'onorbefore'` or `'onorafter'`. ''' return self._relevance @relevance.setter def relevance(self, value): if value == 'before': value = 'prior' if value.lower() not in ('prior', 'after', 'onorbefore', 'onorafter'): raise exceptions21.MetadataException( f'Relevance value is not supported by this object: {value!r}') self._relevance = value.lower() def _prepareData(self, data: DateParseType): r''' Assume a string is supplied as argument ''' # here, using a list to store one object; this provides more # compatibility w/ other formats self._data = [] # clear list self._dataUncertainty = [None] self._data.append(Date()) self._data[0].load(data)
# -----------------------------------------------------------------------------
[docs] class DateBetween(DatePrimitive): r''' Store a relative date, sometime between two dates: >>> dd = metadata.DateBetween(['2009/12/31', '2010/1/28']) >>> str(dd) '2009/12/31 to 2010/01/28' >>> dd = metadata.DateBetween(['2009/12/31', '2010/1/28'], 'certain') Traceback (most recent call last): music21.exceptions21.MetadataException: Relevance value is not supported by this object: 'certain' >>> d1 = metadata.Date(year=1605) >>> d2 = metadata.Date(year=1608, month='11?') >>> dd = metadata.DateBetween([d1, d2]) >>> str(dd) '1605/--/-- to 1608/11?/--' ''' # INITIALIZER # def __init__(self, data: Iterable[DateParseType] = (), relevance='between'): super().__init__(relevance) self._prepareData(data) # SPECIAL METHODS # def __str__(self): msg = [] for d in self._data: msg.append(str(d)) return ' to '.join(msg) # PRIVATE METHODS # def _prepareData(self, data: Iterable[DateParseType]): r''' Assume a list of dates as strings is supplied as argument ''' self._data = [] self._dataUncertainty = [] for part in data: d = Date() d.load(part) self._data.append(d) # a list of Date objects # can look at Date and determine overall error self._dataUncertainty.append(None) # PUBLIC PROPERTIES # @property def relevance(self): ''' The relevance attribute takes only one value: `'between'`. ''' return self._relevance @relevance.setter def relevance(self, value): if value != 'between': raise exceptions21.MetadataException( f'Relevance value is not supported by this object: {value!r}') self._relevance = value
# -----------------------------------------------------------------------------
[docs] class DateSelection(DatePrimitive): r''' Store a selection of dates, or a collection of dates that might all be possible >>> dd = metadata.DateSelection( ... ['2009/12/31', '2010/1/28', '1894/1/28'], ... ) >>> str(dd) '2009/12/31 or 2010/01/28 or 1894/01/28' >>> dd = metadata.DateSelection( ... [1750, '1775/03?'], ... 'and' ... ) >>> str(dd) '1750/--/-- and 1775/03?/--' >>> dd = metadata.DateSelection( ... ['2009/12/31', '2010/1/28'], ... 'certain', ... ) Traceback (most recent call last): music21.exceptions21.MetadataException: Relevance value is not supported by this object: 'certain' Note that '1350 or 1351 and 1375' is not supported yet. ''' # CLASS VARIABLES # isSingle = False # INITIALIZER # def __init__(self, data: Iterable[DateParseType] = (), relevance='or'): super().__init__(relevance) self._prepareData(data) # SPECIAL METHODS # def __str__(self): msg = [] for d in self._data: msg.append(str(d)) return f' {self._relevance} '.join(msg) # PRIVATE METHODS # def _prepareData(self, data: Iterable[DateParseType]): r''' Assume a list of dates as strings is supplied as argument. ''' self._data = [] self._dataUncertainty = [] for part in data: d = Date() d.load(part) self._data.append(d) # a list of Date objects # can look at Date and determine overall error self._dataUncertainty.append(None) # PUBLIC PROPERTIES # @property def relevance(self): ''' The relevance attribute takes only two values: `'or'` or `'and'`. ''' return self._relevance @relevance.setter def relevance(self, value): if value not in ('or', 'and'): raise exceptions21.MetadataException( f'Relevance value is not supported by this object: {value!r}') self._relevance = value
# ----------------------------------------------------------------------------- # This was enhanced in music21 v8 to add an optional encoding scheme (e.g. URI, DCMIPoint, # etc.) as well as whether the text is translated, or in the original language.
[docs] class Text(prebase.ProtoM21Object): r''' One unit of text data: a title, a name, or some other text data. Store the string and a language name or code. This object can be used and/or subclassed for a variety for of text storage. >>> td = metadata.Text('concerto in d', 'en') >>> str(td) 'concerto in d' >>> td.language 'en' ''' # INITIALIZER # def __init__(self, data: str|Text = '', language: str|None = None, isTranslated: bool|None = None, # True, False, or None (unknown) encodingScheme: str|None = None): if isinstance(data, Text): # accessing private attributes here; not desirable self._data: str|Text = data._data self._language: str|None = data._language self.isTranslated: bool|None = data.isTranslated self.encodingScheme: str|None = data.encodingScheme else: self._data = data self._language = language self.isTranslated = isTranslated self.encodingScheme = encodingScheme # SPECIAL METHODS # def __str__(self): if isinstance(self._data, bytes): return self._data.decode('UTF-8') elif isinstance(self._data, str): return self._data else: return str(self._data) def _reprInternal(self): return str(self)
[docs] def __eq__(self, other) -> bool: ''' >>> t1 = metadata.Text('some text') >>> t2 = metadata.Text('some text') >>> t1 == t2 True Language, isTranslated, and encodingScheme must all exactly match for equality. >>> t2 = metadata.Text('some text', language='en') >>> t1 == t2 False >>> t2 = metadata.Text('some text', isTranslated=True) >>> t1 == t2 False >>> t2 = metadata.Text('some text', encodingScheme='scheme42') >>> t1 == t2 False Comparison with non-Text types, including bare strings, will always be considered unequal. >>> t1 == 'some text' False ''' if type(other) is not type(self): return False if self._data != other._data: return False if self.language != other.language: return False if self.isTranslated != other.isTranslated: return False if self.encodingScheme != other.encodingScheme: return False return True
def __lt__(self, other): ''' Allows for alphabetically sorting two elements ''' if type(other) is not type(self): return NotImplemented return ( (self._data, self.language, self.isTranslated, self.encodingScheme) < (other._data, other.language, other.isTranslated, other.encodingScheme) ) # PUBLIC PROPERTIES # @property def language(self): r''' Set the language of the Text stored within. >>> myText = metadata.Text('my text') >>> myText.language = 'en' >>> myText.language 'en' ''' return self._language @language.setter def language(self, value): self._language = value # PUBLIC METHODS #
[docs] def getNormalizedArticle(self): r''' Return a string representation with normalized articles. >>> td = metadata.Text('Ale is Dear, The', language='en') >>> str(td) 'Ale is Dear, The' >>> td.getNormalizedArticle() 'The Ale is Dear' The language will determine whether the article is moved: >>> td.language = 'de' >>> td.getNormalizedArticle() 'Ale is Dear, The' ''' from music21 import text return text.prependArticle(str(self), self._language)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
[docs] class Contributor(prebase.ProtoM21Object): r''' A person that contributed to a work. Can be a composer, lyricist, arranger, or other type of contributor. In MusicXML, these are "creator" elements. >>> td = metadata.Contributor(role='composer', name='Chopin, Fryderyk') >>> td.role 'composer' >>> td.name 'Chopin, Fryderyk' >>> td.relevance 'contributor' >>> td <music21.metadata.primitives.Contributor composer:Chopin, Fryderyk> ''' # CLASS VARIABLES # relevance = 'contributor' # !!!COM: Composer's name. # !!!COA: Attributed composer. # !!!COS: Suspected composer. # !!!COL: Composer abbreviated, alias, # !!!COC: Composer(s) corporate name. # !!!LYR: Lyricist. # !!!LIB: Librettist. # !!!LAR: Arranger. # !!!LOR: Orchestrator. # !!!TRN: Translator of text. # TODO: add editor roleAbbreviationsDict = { 'com': 'composer', 'coa': 'attributedComposer', 'cos': 'suspectedComposer', 'col': 'composerAlias', 'coc': 'composerCorporate', 'lyr': 'lyricist', 'lib': 'librettist', 'lar': 'arranger', 'lor': 'orchestrator', 'trn': 'translator', } roleAbbreviations = roleAbbreviationsDict.keys() roleNames = roleAbbreviationsDict.values() # INITIALIZER # def __init__(self, *, name: str|Text|None = None, names: Iterable[str|Text] = (), role: str|Text|None = None, birth: None|DateSingle|str = None, death: None|DateSingle|str = None, **keywords): self._role = None if role: # stored in self._role self.role = role # validated with property else: self.role = None # a list of Text objects to support various spellings or # language translations self._names: list[Text] = [] if name: # a single if isinstance(name, Text): self._names.append(name) else: self._names.append(Text(name)) if names: # many for n in names: if isinstance(n, Text): self._names.append(n) else: self._names.append(Text(n)) # store the nationality, if known (not currently used) self._nationality: list[Text] = [] self.birth: DateSingle|None = None self.death: DateSingle|None = None if birth is not None: if not isinstance(birth, DateSingle): birthDS = DateSingle(birth) else: birthDS = birth self.birth = birthDS if death is not None: if not isinstance(death, DateSingle): deathDS = DateSingle(death) else: deathDS = death self.death = deathDS def _reprInternal(self): return f'{self.role}:{self.name}' def __str__(self): if not self.name: return '' return self.name
[docs] def __eq__(self, other) -> bool: ''' >>> c1 = metadata.Contributor( ... role='composer', ... name='The Composer', ... birth='1923', ... death='2013' ... ) >>> c2 = metadata.Contributor( ... role='composer', ... name='The Composer', ... birth='1923', ... death='2013' ... ) Names, role, birth, and death must all be identical for equality. >>> c1 == c2 True >>> c2.role = 'lyricist' >>> c1 == c2 False >>> c2 = metadata.Contributor( ... role='composer', ... name='A Composer', ... birth='1923', ... death='2013' ... ) >>> c1 == c2 False >>> c2 = metadata.Contributor( ... role='composer', ... names=['A Composer', 'The Composer'], ... birth='1923', ... death='2013' ... ) >>> c1 == c2 False >>> c2 = metadata.Contributor( ... role='composer', ... name='The Composer', ... birth='1924', ... death='2013' ... ) >>> c1 == c2 False >>> c2 = metadata.Contributor( ... role='composer', ... name='The Composer', ... birth='1923', ... death='2012' ... ) >>> c1 == c2 False Comparison with a non-Contributor object always returns False. >>> c1 == 'The Composer' False ''' if type(other) is not type(self): return False if self._role != other._role: return False if len(self._names) != len(other._names): return False for name, otherName in zip(sorted(self._names), sorted(other._names)): if name != otherName: return False if self.birth != other.birth: return False if self.death != other.death: return False return True
# PUBLIC METHODS #
[docs] def age(self) -> datetime.timedelta|None: r''' Calculate the age at death of the Contributor, returning a datetime.timedelta object. >>> a = metadata.Contributor( ... name='Beethoven, Ludwig van', ... role='composer', ... birth='1770/12/17', ... death='1827/3/26', ... ) >>> a.birth <music21.metadata.primitives.DateSingle 1770/12/17> >>> a.age() datetime.timedelta(days=20552) >>> a.age().days 20552 >>> years = a.age().days // 365 >>> years 56 If the composer is still alive, it returns the composer's current age. >>> shaw = metadata.Contributor( ... name='Shaw, Caroline', ... role='composer', ... birth='1982/08/01', ... ) >>> shaw_years = shaw.age().days // 365 This test will fail in 2067: >>> 36 < shaw_years < 85 True ''' if self.birth is None: return None if self.death is not None: d = self.death.datetime b = self.birth.datetime return d - b else: return datetime.datetime.now() - self.birth.datetime
# PUBLIC PROPERTIES # @property def name(self): r''' Returns the text name, or the first of many names entered. >>> td = metadata.Contributor( ... role='composer', ... names=['Chopin, Fryderyk', 'Chopin, Frederick'], ... ) >>> td.name 'Chopin, Fryderyk' >>> td.names ['Chopin, Fryderyk', 'Chopin, Frederick'] ''' # return first name if self._names: return str(self._names[0]) else: return None @name.setter def name(self, value): # set first name self._names = [] # reset self._names.append(Text(value)) @property def names(self): r''' Returns all names in a list. >>> td = metadata.Contributor( ... role='composer', ... names=['Chopin, Fryderyk', 'Chopin, Frederick'], ... ) >>> td.names ['Chopin, Fryderyk', 'Chopin, Frederick'] >>> td.names = ['Czerny', 'Spohr'] >>> td.names ['Czerny', 'Spohr'] ''' # return first name msg = [] for n in self._names: msg.append(str(n)) return msg @names.setter def names(self, values): if not common.isIterable(values): raise exceptions21.MetadataException( '.names must be a list -- do you mean .name instead?') self._names = [] # reset for n in values: self._names.append(Text(n)) @property def role(self): r''' The role is what part this Contributor plays in the work. Both full roll strings and roll abbreviations may be used. >>> td = metadata.Contributor() >>> td.role = 'composer' >>> td.role 'composer' In case of a Humdrum role abbreviation, the role that is set is the full name: >>> td.role = 'lor' >>> td.role 'orchestrator' Roles can be created on the fly: >>> td.role = 'court jester' >>> td.role 'court jester' ''' return self._role @role.setter def role(self, value): if value is None or value in self.roleAbbreviationsDict.values(): self._role = value elif value in self.roleAbbreviationsDict: self._role = self.roleAbbreviationsDict[value] else: self._role = value
[docs] @staticmethod def abbreviationToRole(abbreviation): r''' Convert `abbreviation` to role name: >>> metadata.Contributor.abbreviationToRole('com') 'composer' >>> metadata.Contributor.abbreviationToRole('lib') 'librettist' ''' abbreviation = abbreviation.lower() if abbreviation in Contributor.roleAbbreviationsDict: return Contributor.roleAbbreviationsDict[abbreviation] else: raise exceptions21.MetadataException( f'no such role: {abbreviation!r}')
[docs] @staticmethod def roleToAbbreviation(roleName): ''' Convert `roleName` to role abbreviation: >>> metadata.Contributor.roleToAbbreviation('composer') 'com' ''' # note: probably not the fastest way to do this for role_id in Contributor.roleAbbreviationsDict: if roleName.lower() == Contributor.roleAbbreviationsDict[role_id].lower(): return role_id raise exceptions21.MetadataException(f'No such role: {roleName}')
# -----------------------------------------------------------------------------
[docs] class Creator(Contributor): r''' A person that created a work. Can be a composer, lyricist, arranger, or other type of contributor. In MusicXML, these are "creator" elements. >>> td = metadata.Creator(role='composer', name='Chopin, Fryderyk') >>> td.role 'composer' >>> td.name 'Chopin, Fryderyk' >>> td.relevance 'creator' ''' # CLASS VARIABLES # relevance = 'creator'
# -----------------------------------------------------------------------------
[docs] class Imprint(prebase.ProtoM21Object): r''' An object representation of imprint, or publication. ''' def __init__(self, **keywords): self.keywords = keywords
# !!!PUB: Publication status. # !!!PPR: First publisher. # !!!PDT: Date first published. # !!!PPP: Place first published. # !!!PC#: Publisher's catalogue number. # !!!SCT: Scholarly catalogue abbreviation and number. E.g. BWV 551 # !!!SCA: Scholarly catalogue (unabbreviated) name. E.g.Köchel 117. # !!!SMS: Manuscript source name. # !!!SML: Manuscript location. # !!!SMA: Acknowledgement of manuscript access. # ----------------------------------------------------------------------------- # !!!YEP: Publisher of electronic edition. # !!!YEC: Date and owner of electronic copyright. # !!!YER: Date electronic edition released. # !!!YEM: Copyright message. # !!!YEN: Country of copyright. # !!!YOR: Original document. # !!!YOO: Original document owner. # !!!YOY: Original copyright year. # !!!YOE: Original editor. # ----------------------------------------------------------------------------- # supported work ids and abbreviations # 'otl' : 'title', # 'otp' : 'popularTitle', # 'ota' : 'alternativeTitle', # 'opr' : 'parentTitle', # 'oac' : 'actNumber', # # 'osc' : 'sceneNumber', # 'omv' : 'movementNumber', # 'omd' : 'movementName', # 'ops' : 'opusNumber', # 'onm' : 'number', # # 'ovm' : 'volume', # 'ode' : 'dedication', # 'oco' : 'commission', # 'gtl' : 'groupTitle', # 'gaw' : 'associatedWork', # # 'gco' : 'collectionDesignation', # 'txo' : 'textOriginalLanguage', # 'txl' : 'textLanguage', # # 'ocy' : 'countryOfComposition', # 'opc' : 'localeOfComposition', # origin in abc # ----------------------------------------------------------------------------- class Test(unittest.TestCase): def testText(self): from music21 import metadata text = metadata.primitives.Text('my text') text.language = 'en' self.assertEqual(text._data, 'my text') self.assertEqual(text._language, 'en') def testContributor(self): from music21 import metadata contributor = metadata.primitives.Contributor( role='composer', name='Gilles Binchois', ) self.assertEqual(contributor.role, 'composer') self.assertEqual(contributor.relevance, 'contributor') self.assertEqual(contributor.name, 'Gilles Binchois') def testCreator(self): from music21 import metadata creator = metadata.primitives.Creator( role='composer', name='Gilles Binchois', ) self.assertEqual(creator.role, 'composer') self.assertEqual(creator.relevance, 'creator') self.assertEqual(creator.name, 'Gilles Binchois') def testDate(self): from music21 import metadata date1 = metadata.primitives.Date(year=1843, yearError='approximate') date2 = metadata.primitives.Date(year='1843?') self.assertEqual(date1.year, 1843) self.assertEqual(date1.yearError, 'approximate') self.assertEqual(date2.year, 1843) self.assertEqual(date2.yearError, 'uncertain') def testDateValueError(self): with self.assertRaisesRegex(ValueError, 'Month must be.*not 13'): Date(month=13) for d, m, y in ((32, None, None), (0, None, None), (31, 4, None), (30, 2, None), (29, 2, 1999), ): with self.assertRaisesRegex(ValueError, 'Day.*is not possible'): Date(year=y, month=m, day=d) with self.assertRaisesRegex(ValueError, 'Hour'): Date(hour=24) with self.assertRaisesRegex(ValueError, 'Minute'): Date(minute=61) with self.assertRaisesRegex(ValueError, 'Second'): Date(second=-1) self.assertIsNotNone(Date(year=2000, month=2, day=29)) self.assertIsNotNone(Date(month=2, day=29)) self.assertIsNotNone(Date(month=12, day=31)) self.assertIsNotNone(Date(hour=23, minute=59, second=59)) def testDateSingle(self): from music21 import metadata dateSingle = metadata.primitives.DateSingle( '2009/12/31', 'approximate') self.assertEqual(str(dateSingle), '2009/12/31') self.assertEqual(len(dateSingle._data), 1) self.assertEqual(dateSingle._relevance, 'approximate') self.assertEqual(dateSingle._dataUncertainty, ['approximate']) def testDateRelative(self): from music21 import metadata dateRelative = metadata.primitives.DateRelative('2001/12/31', 'prior') self.assertEqual(str(dateRelative), 'prior to 2001/12/31') self.assertEqual(dateRelative.relevance, 'prior') self.assertEqual(len(dateRelative._data), 1) self.assertEqual(dateRelative._dataUncertainty, [None]) def testDateBetween(self): from music21 import metadata dateBetween = metadata.primitives.DateBetween( ('2009/12/31', '2010/1/28')) self.assertEqual(str(dateBetween), '2009/12/31 to 2010/01/28') self.assertEqual(dateBetween.relevance, 'between') self.assertEqual(dateBetween._dataUncertainty, [None, None]) self.assertEqual(len(dateBetween._data), 2) def testDateSelection(self): from music21 import metadata dateSelection = metadata.primitives.DateSelection( ['2009/12/31', '2010/1/28', '1894/1/28'], 'or', ) self.assertEqual(str(dateSelection), '2009/12/31 or 2010/01/28 or 1894/01/28') self.assertEqual(dateSelection.relevance, 'or') self.assertEqual(dateSelection._dataUncertainty, [None, None, None]) self.assertEqual(len(dateSelection._data), 3) # ----------------------------------------------------------------------------- _DOC_ORDER = ( Text, Date, DateSingle, DateRelative, DateBetween, DateSelection, Contributor, Copyright, ) type DateParseType = Date|datetime.datetime|str type ValueType = DatePrimitive|Text|Contributor|Copyright|int if __name__ == '__main__': import music21 music21.mainTest(Test) # -----------------------------------------------------------------------------