Source code for abjad.bind

"""
Functions to bind indicators to score components.
"""

import enum
import typing

from . import duration as _duration
from . import enums as _enums
from . import exceptions as _exceptions
from . import score as _score
from . import tag as _tag
from . import tweaks as _tweaks
from . import wrapper as _wrapper


def _before_attach(
    indicator: typing.Any,
    context: str | None,
    deactivate: bool,
    hide: bool,
    synthetic_offset: _duration.Offset | None,
    component: _score.Component,
) -> None:
    if getattr(indicator, "temporarily_do_not_check", False) is True:
        return
    if hasattr(indicator, "allowable_sites"):
        if indicator.site not in component._allowable_sites:
            message = f"\n  {indicator!r}"
            message += f"\n  Can not attach to {component.__class__.__name__}."
            message += f"\n  {component.__class__.__name__} allows only "
            message += ", ".join(component._allowable_sites)
            message += " sites."
            raise Exception(message)
    if not hasattr(indicator, "context"):
        return
    if getattr(indicator, "find_context_on_attach", False) is True:
        the_context = context or indicator.context
        if the_context is not None:
            context_ = _wrapper.Wrapper._find_correct_effective_context(
                component, the_context
            )
            if context_ is None:
                message = f"\n    {indicator} requires {the_context} context;"
                message += f"\n    can not find {the_context}"
                message += f" in parentage of {component!r}."
                raise _exceptions.MissingContextError(message)
    if getattr(indicator, "nestable_spanner", False) is True:
        return
    if deactivate is True:
        return
    if synthetic_offset is not None:
        return
    for wrapper in component._get_wrappers():
        if not isinstance(wrapper.unbundle_indicator(), type(indicator)):
            continue
        if getattr(indicator, "leak", None) != getattr(
            wrapper.unbundle_indicator(), "leak", None
        ):
            continue
        if indicator != wrapper.unbundle_indicator():
            if (
                getattr(indicator, "allow_multiple_with_different_values", False)
                is True
            ):
                continue
            if hide != wrapper.hide():
                continue
            if getattr(indicator, "site", None) != getattr(
                wrapper.unbundle_indicator(), "site", None
            ):
                continue
        classname = type(component).__name__
        message = f"attempting to attach conflicting indicator to {classname}:"
        message += "\n  Already attached:"
        message += f"\n    {wrapper.unbundle_indicator()!r}"
        message += "\n  Attempting to attach:"
        message += f"\n    {indicator!r}"
        raise _exceptions.PersistentIndicatorError(message)


def _unsafe_attach(
    indicator: typing.Any,
    component,
    *,
    check_duplicate_indicator: bool = False,
    context: str | None = None,
    deactivate: bool = False,
    direction: _enums.Vertical | None = None,
    do_not_test: bool = False,
    hide: bool = False,
    synthetic_offset: _duration.Offset | None = None,
    tag: _tag.Tag | None = None,
) -> None:
    if isinstance(indicator, _tweaks.Bundle):
        nonbundle_indicator = indicator.indicator
    else:
        nonbundle_indicator = indicator
    assert not isinstance(nonbundle_indicator, _tweaks.Bundle)
    if isinstance(nonbundle_indicator, _tag.Tag):
        message = "use the tag=None keyword instead of attach():\n"
        message += f"   {repr(indicator)}"
        raise Exception(message)
    assert isinstance(component, _score.Component), repr(component)
    if tag is not None and not isinstance(tag, _tag.Tag):
        raise Exception(f"must be be tag: {repr(tag)}")
    assert nonbundle_indicator is not None, repr(nonbundle_indicator)
    assert isinstance(component, _score.Component), repr(component)
    grace_prototype = (_score.AfterGraceContainer, _score.BeforeGraceContainer)
    if context is not None and isinstance(nonbundle_indicator, grace_prototype):
        raise Exception(f"set context only for indicators, not {indicator!r}.")
    if deactivate is True and tag is None:
        raise Exception("tag must exist when deactivate is true.")
    if hasattr(nonbundle_indicator, "_attachment_test_all") and not do_not_test:
        result = nonbundle_indicator._attachment_test_all(component)
        if result is not True:
            assert isinstance(result, list), repr(result)
            result = ["  " + _ for _ in result]
            message = f"{nonbundle_indicator!r}._attachment_test_all():"
            result.insert(0, message)
            message = "\n".join(result)
            raise Exception(message)
    if isinstance(nonbundle_indicator, grace_prototype):
        if not isinstance(component, _score.Leaf):
            raise Exception("grace containers attach to single leaf only.")
        nonbundle_indicator._attach(component)
        return None
    if isinstance(component, _score.Container):
        acceptable = False
        if isinstance(
            nonbundle_indicator, dict | str | enum.Enum | _tag.Tag | _wrapper.Wrapper
        ):
            acceptable = True
        if getattr(nonbundle_indicator, "can_attach_to_containers", False):
            acceptable = True
        if not acceptable:
            message = f"can not attach {indicator!r} to containers: {component!r}"
            raise Exception(message)
    elif not isinstance(component, _score.Leaf):
        message = f"indicator {indicator!r} must attach to leaf, not {component!r}."
        raise Exception(message)
    annotation = None
    if isinstance(indicator, _wrapper.Wrapper):
        annotation = indicator.annotation()
        context = context or indicator.context_name()
        deactivate = deactivate or indicator.deactivate()
        hide = hide or indicator.hide()
        synthetic_offset = synthetic_offset or indicator.synthetic_offset()
        tag = tag or indicator.tag()
        indicator._detach()
        indicator = indicator.indicator()
    if hasattr(nonbundle_indicator, "context"):
        context = context or nonbundle_indicator.context
    if tag is None:
        tag = _tag.Tag()
    _wrapper.Wrapper(
        annotation=annotation,
        check_duplicate_indicator=check_duplicate_indicator,
        component=component,
        context_name=context,
        deactivate=deactivate,
        direction=direction,
        hide=hide,
        indicator=indicator,
        synthetic_offset=synthetic_offset,
        tag=tag,
    )


# TODO: remove abjad.Wrapper.annotation;
#       store annotations in abjad.Component._annotations dictionary instead;
#       annotations do not need context_name, hide, synthetic_offset, etc.
[docs] def annotate(component: _score.Component, key: str, value: object) -> None: r""" Annotates ``component`` with ``key`` equal to ``value``. .. container:: example Annotations do not affect LilyPond output. >>> staff = abjad.Staff("c'4 d' e' f'") >>> abjad.annotate(staff[0], "motive_number", 6) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff) >>> print(string) \new Staff { c'4 d'4 e'4 f'4 } >>> abjad.get.annotation(staff[0], "motive_number") 6 """ assert isinstance(component, _score.Component), repr(component) assert isinstance(key, str), repr(key) _wrapper.Wrapper(annotation=key, component=component, indicator=value)
[docs] def attach( indicator: typing.Any, component: _score.Component, *, check_duplicate_indicator: bool = False, # TODO: change `context` to `context_name` context: str | None = None, deactivate: bool = False, # TODO: remove _enums.Vertical and consolidate direction in some other way direction: _enums.Vertical | None = None, do_not_test: bool = False, hide: bool = False, synthetic_offset: _duration.Offset | None = None, tag: _tag.Tag | None = None, ) -> None: r""" Attaches ``indicator`` to ``component``. .. container:: example The class of ``component`` is almost always a leaf. (A small number of indicators can attach to both leaves and containers.) Acceptable types of ``indicator``: :: * indicator * abjad.Wrapper * abjad.BeforeGraceContainer (DEPRECATED) * abjad.AfterGraceContainer (DEPRECATED) .. container:: example Attaches articulation to last note in staff: >>> staff = abjad.Staff("c'4 d' e' f'") >>> articulation = abjad.Articulation(">") >>> abjad.attach(articulation, staff[-1]) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff) >>> print(string) \new Staff { c'4 d'4 e'4 f'4 - \accent } .. container:: example The ``check_duplicate_indicator=False`` keyword. Consider the case of a score with two staves. In the usual case, it is necessary to attach a metronome mark to the first note of only one of the two staves: >>> staff_1 = abjad.Staff("c''4 d''4 e''4 f''4") >>> staff_2 = abjad.Staff("c'4 d'4 e'4 f'4") >>> score = abjad.Score([staff_1, staff_2]) >>> metronome_mark = abjad.MetronomeMark(abjad.Duration(1, 4), 52) >>> abjad.attach(metronome_mark, staff_1[0]) >>> abjad.show(score) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(score) >>> print(string) \new Score << \new Staff { \tempo 4=52 c''4 d''4 e''4 f''4 } \new Staff { c'4 d'4 e'4 f'4 } >> But what should happen when a conflicting metronome mark is attached at the same moment in a different context? Abjad allows this behavior, but it is not clear what it should mean to have two metronome marks in effect at the same time. Set ``check_duplicate_indicators=True`` to raise an exception instead. .. container:: example The ``context=None`` keyword. Indicators that affect many notes, rests or chords in a row define the context at which they take effect. Clefs effect all notes on a staff, for example, which is why attaching a clef to the first note in a staff effects all the others: >>> clef = abjad.Clef("alto") >>> clef.context 'Staff' >>> staff = abjad.Staff("c'4 d' e' f'") >>> abjad.attach(clef, staff[0]) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff) >>> print(string) \new Staff { \clef "alto" c'4 d'4 e'4 f'4 } >>> for leaf in abjad.select.leaves(staff): ... leaf, abjad.get.effective_indicator(leaf, abjad.Clef) ... (Note("c'4"), Clef(name='alto')) (Note("d'4"), Clef(name='alto')) (Note("e'4"), Clef(name='alto')) (Note("f'4"), Clef(name='alto')) This example sets ``context="MusicStaff"`` to show that the alto clef governs all notes in a custom staff context (rather than the default staff context): >>> voice = abjad.Voice("c'4 d' e' f'", name="MusicVoice") >>> staff = abjad.Staff([voice], name="MusicStaff") >>> clef = abjad.Clef("alto") >>> abjad.attach(clef, voice[0], context="MusicStaff") >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff) >>> print(string) \context Staff = "MusicStaff" { \context Voice = "MusicVoice" { \clef "alto" c'4 d'4 e'4 f'4 } } >>> for leaf in abjad.select.leaves(staff): ... leaf, abjad.get.effective_indicator(leaf, abjad.Clef) ... (Note("c'4"), Clef(name='alto')) (Note("d'4"), Clef(name='alto')) (Note("e'4"), Clef(name='alto')) (Note("f'4"), Clef(name='alto')) .. container:: example If multiple contexted indicators are attached at the same offset then ``abjad.attach()`` raises ``abjad.PersistentIndicatorError`` if all indicators are active. But simultaneous contexted indicators are allowed if only one is active (and all others are inactive): >>> staff = abjad.Staff("c'4 d' e' f'") >>> abjad.attach(abjad.Clef("treble"), staff[0]) >>> abjad.attach( ... abjad.Clef("alto"), ... staff[0], ... deactivate=True, ... tag=abjad.Tag("+PARTS"), ... ) >>> abjad.attach( ... abjad.Clef("tenor"), ... staff[0], ... deactivate=True, ... tag=abjad.Tag("+PARTS"), ... ) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff, tags=True) >>> print(string) \new Staff { %! +PARTS %@% \clef "alto" %! +PARTS %@% \clef "tenor" \clef "treble" c'4 d'4 e'4 f'4 } Active indicator is always effective when competing inactive indicators are present: >>> for note in staff: ... clef = abjad.get.effective_indicator(staff[0], abjad.Clef) ... note, clef ... (Note("c'4"), Clef(name='treble')) (Note("d'4"), Clef(name='treble')) (Note("e'4"), Clef(name='treble')) (Note("f'4"), Clef(name='treble')) But a lone inactivate indicator is effective when no active indicator is present. Note that ``tag`` must be an ``abjad.Tag`` when ``deactivate=True``: >>> staff = abjad.Staff("c'4 d' e' f'") >>> abjad.attach( ... abjad.Clef("alto"), ... staff[0], ... deactivate=True, ... tag=abjad.Tag("+PARTS"), ... ) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff, tags=True) >>> print(string) \new Staff { %! +PARTS %@% \clef "alto" c'4 d'4 e'4 f'4 } >>> for note in staff: ... clef = abjad.get.effective_indicator(staff[0], abjad.Clef) ... note, clef ... (Note("c'4"), Clef(name='alto')) (Note("d'4"), Clef(name='alto')) (Note("e'4"), Clef(name='alto')) (Note("f'4"), Clef(name='alto')) """ if isinstance(indicator, _tweaks.Bundle): nonbundle_indicator = indicator.indicator else: nonbundle_indicator = indicator assert not isinstance(nonbundle_indicator, _tweaks.Bundle) assert nonbundle_indicator is not None, repr(nonbundle_indicator) assert isinstance(component, _score.Component), repr(component) assert isinstance(hide, bool), repr(hide) _before_attach( nonbundle_indicator, context, deactivate, hide, synthetic_offset, component, ) _unsafe_attach( indicator, component, check_duplicate_indicator=check_duplicate_indicator, context=context, deactivate=deactivate, direction=direction, do_not_test=do_not_test, hide=hide, synthetic_offset=synthetic_offset, tag=tag, )
[docs] def detach(indicator, component: _score.Component, *, by_id: bool = False) -> tuple: r""" Detaches indicators equals to ``indicator`` from ``component``. Returns tuple of zero or more detached items. .. container:: example Detaches articulations from first note in staff: >>> staff = abjad.Staff("c'4 d' e' f'") >>> abjad.attach(abjad.Articulation(">"), staff[0]) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff) >>> print(string) \new Staff { c'4 - \accent d'4 e'4 f'4 } >>> abjad.detach(abjad.Articulation, staff[0]) (Articulation(name='>'),) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff) >>> print(string) \new Staff { c'4 d'4 e'4 f'4 } .. container:: example Set ``by_id`` to true to detach exact ``indicator`` from ``component`` (rather than detaching all indicatorslequal to ``indicator``). The use of ``by_id`` is motivated by the following. Consider the three document-specifier markups below: >>> markup_1 = abjad.Markup(r"\markup tutti") >>> markup_2 = abjad.Markup(r"\markup { with the others }") >>> markup_3 = abjad.Markup(r"\markup { with the others }") Markups two and three compare equal: >>> markup_2 == markup_3 True But document-tagging like this makes sense for score and two diferent parts: >>> staff = abjad.Staff("c'4 d' e' f'") >>> tag = abjad.Tag(string="+SCORE") >>> abjad.attach(markup_1, staff[0], direction=abjad.UP, tag=tag) >>> abjad.attach( ... markup_2, ... staff[0], ... deactivate=True, ... direction=abjad.UP, ... tag=abjad.Tag("+PARTS_VIOLIN_1"), ... ) >>> abjad.attach( ... markup_3, ... staff[0], ... deactivate=True, ... direction=abjad.UP, ... tag=abjad.Tag("+PARTS_VIOLIN_2"), ... ) >>> abjad.show(staff) # doctest: +SKIP >>> string = abjad.lilypond(staff, tags=True) >>> print(string) \new Staff { c'4 %! +SCORE ^ \markup tutti %! +PARTS_VIOLIN_1 %@% ^ \markup { with the others } %! +PARTS_VIOLIN_2 %@% ^ \markup { with the others } d'4 e'4 f'4 } The question is then how to detach just one of the two markups that compare equal to each other? Passing in one of the markup objects directory doesn't work. This is because detach tests for equality to input argument: >>> markups = abjad.detach(markup_2, staff[0]) >>> for markup in markups: ... markup Markup(string='\\markup { with the others }') Markup(string='\\markup { with the others }') >>> abjad.show(staff) # doctest: +SKIP >>> string = abjad.lilypond(staff, tags=True) >>> print(string) \new Staff { c'4 %! +SCORE ^ \markup tutti d'4 e'4 f'4 } We start again: >>> staff = abjad.Staff("c'4 d' e' f'") >>> tag = abjad.Tag(string="+SCORE") >>> abjad.attach(markup_1, staff[0], direction=abjad.UP, tag=tag) >>> abjad.attach( ... markup_2, ... staff[0], ... deactivate=True, ... direction=abjad.UP, ... tag=abjad.Tag("+PARTS_VIOLIN_1"), ... ) >>> abjad.attach( ... markup_3, ... staff[0], ... deactivate=True, ... direction=abjad.UP, ... tag=abjad.Tag("+PARTS_VIOLIN_2"), ... ) >>> abjad.show(staff) # doctest: +SKIP >>> string = abjad.lilypond(staff, tags=True) >>> print(string) \new Staff { c'4 %! +SCORE ^ \markup tutti %! +PARTS_VIOLIN_1 %@% ^ \markup { with the others } %! +PARTS_VIOLIN_2 %@% ^ \markup { with the others } d'4 e'4 f'4 } This time we set ``by_id`` to true. Now detach checks the exact id of its input argument (rather than just testing for equality). This gives us what we want: >>> markups = abjad.detach(markup_2, staff[0], by_id=True) >>> for markup in markups: ... markup Markup(string='\\markup { with the others }') >>> abjad.show(staff) # doctest: +SKIP >>> string = abjad.lilypond(staff, tags=True) >>> print(string) \new Staff { c'4 %! +SCORE ^ \markup tutti %! +PARTS_VIOLIN_2 %@% ^ \markup { with the others } d'4 e'4 f'4 } .. container:: example REGRESSION. Attach-detach-attach pattern works correctly when detaching wrappers: >>> staff = abjad.Staff("c'4 d' e' f'") >>> abjad.attach(abjad.Clef("alto"), staff[0]) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff) >>> print(string) \new Staff { \clef "alto" c'4 d'4 e'4 f'4 } >>> wrapper = abjad.get.wrappers(staff[0])[0] >>> wrappers = abjad.detach(wrapper, wrapper.component()) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff) >>> print(string) \new Staff { c'4 d'4 e'4 f'4 } >>> abjad.attach(abjad.Clef("tenor"), staff[0]) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff) >>> print(string) \new Staff { \clef "tenor" c'4 d'4 e'4 f'4 } """ assert isinstance(component, _score.Component), repr(component) assert isinstance(by_id, bool), repr(by_id) after_grace_container = None before_grace_container = None if isinstance(indicator, type): if "AfterGraceContainer" in indicator.__name__: assert isinstance(component, _score.Leaf) after_grace_container = component._after_grace_container elif "BeforeGraceContainer" in indicator.__name__: assert isinstance(component, _score.Leaf) before_grace_container = component._before_grace_container else: assert hasattr(component, "_wrappers") result = [] for wrapper in component._wrappers[:]: if isinstance(wrapper, indicator): component._wrappers.remove(wrapper) result.append(wrapper) elif isinstance(wrapper.unbundle_indicator(), indicator): wrapper._detach() result.append(wrapper.indicator()) return tuple(result) else: if "AfterGraceContainer" in indicator.__class__.__name__: assert isinstance(component, _score.Leaf) after_grace_container = component._after_grace_container elif "BeforeGraceContainer" in indicator.__class__.__name__: assert isinstance(component, _score.Leaf) before_grace_container = component._before_grace_container else: assert hasattr(component, "_wrappers") result = [] for wrapper in component._wrappers[:]: if wrapper is indicator: wrapper._detach() result.append(wrapper) elif wrapper.unbundle_indicator() == indicator: if by_id is True and id(indicator) != id( wrapper.unbundle_indicator() ): pass else: wrapper._detach() result.append(wrapper.indicator()) return tuple(result) items: list[typing.Any] = [] if after_grace_container is not None: items.append(after_grace_container) if before_grace_container is not None: items.append(before_grace_container) if by_id is True: items = [_ for _ in items if id(_) == id(indicator)] for item in items: item._detach() return tuple(items)