Source code for abjad.wrapper

"""
Wrapper.
"""

import copy
import importlib
import typing

from . import _getlib, _updatelib
from . import duration as _duration
from . import enums as _enums
from . import exceptions as _exceptions
from . import parentage as _parentage
from . import score as _score
from . import tag as _tag
from . import tweaks as _tweaks


[docs] class Wrapper: r""" Wrapper. .. container:: example Use ``abjad.get.attach()`` to attach an indicator to a component. Use ``abjad.get.wrapper()`` to inspect the wrapper that is created. >>> component = abjad.Note("c'4") >>> articulation = abjad.Articulation("accent") >>> abjad.attach(articulation, component, direction=abjad.UP) >>> wrapper = abjad.get.wrapper(component) >>> type(wrapper) <class 'abjad.wrapper.Wrapper'> """ __slots__ = ( "_annotation", "_component", "_context_name", "_deactivate", "_direction", "_effective_context", "_hide", "_indicator", "_synthetic_offset", "_tag", ) def __init__( self, *, annotation: str | None = None, check_duplicate_indicator: bool = False, component: _score.Component | None = None, context_name: str | None = None, deactivate: bool = False, direction: _enums.Vertical | str | None = None, hide: bool = False, indicator: typing.Any = None, synthetic_offset: _duration.Offset | None = None, tag: _tag.Tag = _tag.Tag(), ) -> None: if annotation is not None: assert isinstance(annotation, str), repr(annotation) if component is not None: assert isinstance(component, _score.Component), repr(component) if context_name is not None: assert isinstance(context_name, str), repr(context_name) assert isinstance(deactivate, bool), repr(deactivate) if direction is not None: assert isinstance(direction, _enums.Vertical | str), repr(direction) assert isinstance(hide, bool), repr(hide) assert not isinstance(indicator, type(self)), repr(indicator) if synthetic_offset is not None: prototype = _duration.Offset assert isinstance(synthetic_offset, prototype), repr(synthetic_offset) assert isinstance(tag, _tag.Tag), repr(tag) self._annotation = annotation self._component = component self._context_name = context_name self._deactivate = deactivate self._direction = direction self._effective_context: _score.Context | None = None self._hide = hide self._indicator = indicator self._synthetic_offset = synthetic_offset self._tag = tag if component is not None: self._bind_component( component, check_duplicate_indicator=check_duplicate_indicator, ) def __copy__(self, *arguments) -> "Wrapper": """ Copies all properties except component; calling code must supply component after copy. """ new = type(self)( annotation=self.annotation(), component=None, context_name=self.context_name(), deactivate=self.deactivate(), direction=self.direction(), hide=self.hide(), indicator=copy.copy(self.indicator()), synthetic_offset=self.synthetic_offset(), tag=self.tag(), ) return new def __eq__(self, argument) -> bool: if not isinstance(argument, Wrapper): return False if self.annotation() != argument.annotation(): return False if self.component() != argument.component(): return False if self.context_name() != argument.context_name(): return False if self.deactivate() != argument.deactivate(): return False if self.hide() != argument.hide(): return False if self.indicator() != argument.indicator(): return False if self.synthetic_offset() != argument.synthetic_offset(): return False if self.tag() != argument.tag(): return False return True def __hash__(self) -> int: return hash(self.__class__.__name__ + str(self)) def __repr__(self) -> str: parameters = f""" annotation={self.annotation()!r}, context_name={self.context_name()!r}, deactivate={self.deactivate()!r}, direction={self.direction()!r}, hide={self.hide()!r}, indicator={self.indicator()!r}, synthetic_offset={self.synthetic_offset()!r}, tag={self.tag()!r} """ parameters = " ".join(parameters.split()) return f"{type(self).__name__}({parameters})" def _bind_component( self, component: _score.Component, *, check_duplicate_indicator: bool = False, ) -> None: indicator = self.unbundle_indicator() if getattr(indicator, "context", None) is not None: if check_duplicate_indicator is True: self._check_duplicate_indicator(component) self._unbind_component() self._component = component self._update_effective_context() if getattr(indicator, "mutates_offsets_in_seconds", False): self._component._update_later(offsets_in_seconds=True) component._wrappers.append(self) def _bind_effective_context( self, correct_effective_context: _score.Context | None, ) -> None: if correct_effective_context is not None: assert isinstance(correct_effective_context, _score.Context) self._unbind_effective_context() if correct_effective_context is not None: if self not in correct_effective_context._dependent_wrappers: correct_effective_context._dependent_wrappers.append(self) self._effective_context = correct_effective_context self._update_effective_context() indicator = self.unbundle_indicator() if correct_effective_context is not None: if getattr(indicator, "mutates_offsets_in_seconds", False): correct_effective_context._update_later(offsets_in_seconds=True) def _check_duplicate_indicator(self, component: _score.Component) -> None: if self.deactivate() is True: return indicator = self.unbundle_indicator() prototype = type(indicator) command = getattr(indicator, "command", None) wrapper = _getlib._get_effective_wrapper( component, prototype, attributes={"command": command}, ) wrapper_site = None if wrapper is not None: wrapper_site = getattr(wrapper.unbundle_indicator(), "site", None) my_site = getattr(indicator, "site", None) if ( wrapper is None or wrapper.context_name() is None or wrapper.deactivate() is True or wrapper.start_offset() != self.start_offset() or wrapper_site != my_site ): return my_leak = getattr(indicator, "leak", None) if getattr(wrapper.unbundle_indicator(), "leak", None) != my_leak: return context = None for parent in component._get_parentage(): if hasattr(parent, "_lilypond_type"): context = parent break wrapper_context = None for parent in wrapper.component()._get_parentage(): if hasattr(parent, "_lilypond_type"): wrapper_context = parent break if wrapper.unbundle_indicator() == indicator and context is not wrapper_context: return assert isinstance(wrapper_context, _score.Context) message = f"\n\nCan not attach ...\n\n{repr(self)}\n\n..." message += f" to {repr(component)}" # message += f" in {getattr(context, 'name', None)} because ..." assert context is not None message += f" in {context.name()} because ..." message += f"\n\n{repr(wrapper)}\n\n" message += "... is already attached" if component is wrapper.component(): message += " to the same leaf." else: message += f" to {repr(wrapper.component())}" message += f" in {wrapper_context.name()}." message += "\n" raise _exceptions.PersistentIndicatorError(message) def _detach(self) -> "Wrapper": self._unbind_component() self._unbind_effective_context() return self @staticmethod def _find_correct_effective_context( component: _score.Component | None, context_name: str, ) -> _score.Context | None: assert isinstance(component, _score.Component), repr(component) assert isinstance(context_name, str), repr(context_name) abjad = importlib.import_module("abjad") context = getattr(abjad, context_name, None) candidate = None parentage = _parentage.Parentage(component) if context is not None: assert issubclass(context, _score.Context), repr(context) candidate = parentage.get(context) else: for component in parentage: assert component is not None if hasattr(component, "name") and component.name() == context_name: candidate = component break if hasattr(component, "lilypond_type") and ( component.lilypond_type() == context_name ): candidate = component break if candidate.__class__.__name__ == "Voice": for component in reversed(parentage): if not component.__class__.__name__ == "Voice": continue assert isinstance(component, _score.Voice) assert isinstance(candidate, _score.Voice) if component.name() == candidate.name(): candidate = component break assert isinstance(candidate, _score.Context | None), repr(candidate) return candidate def _get_effective_context(self) -> _score.Context | None: if self.component() is not None: _updatelib._update_now(self.component(), indicators=True) return self._effective_context def _unbind_component(self) -> None: component = self.component() if component is not None and id(self) in [id(_) for _ in component._wrappers]: component._wrappers.remove(self) self._component = None def _unbind_effective_context(self) -> None: if ( self._effective_context is not None and self in self._effective_context._dependent_wrappers ): self._effective_context._dependent_wrappers.remove(self) self._effective_context = None def _update_effective_context(self) -> None: context_name = self.context_name() if context_name is not None: correct_effective_context = self._find_correct_effective_context( self.component(), context_name, ) else: correct_effective_context = None if self._effective_context is not correct_effective_context: self._bind_effective_context(correct_effective_context) if correct_effective_context is not None: if self not in correct_effective_context._dependent_wrappers: correct_effective_context._dependent_wrappers.append(self)
[docs] def annotation(self) -> str | None: """ Gets annotation with which indicator is attached to component. """ return self._annotation
[docs] def component(self) -> _score.Component | None: """ Gets component to which indicator is attached. """ return self._component
[docs] def context_name(self) -> str | None: """ Gets name of context at which indicator is attached to component. """ return self._context_name
[docs] def deactivate(self) -> bool: """ Is true when indicator is deactivated in LilyPond output. """ return self._deactivate
[docs] def set_deactivate(self, argument: bool) -> None: """ Sets wrapper ``deactivate`` flag. """ assert isinstance(argument, bool), repr(argument) self._deactivate = argument
# TODO: typehint
[docs] def direction(self): """ Gets direction of indicator. """ return self._direction
[docs] def hide(self) -> bool: """ Is true when indcator does not appear in LilyPond output. """ return self._hide
[docs] def indicator(self) -> typing.Any: """ Gets indicator. """ return self._indicator
[docs] def is_bundled(self) -> bool: """ Is true when indicator is bundled. """ return isinstance(self.indicator(), _tweaks.Bundle)
[docs] def leaked_start_offset(self) -> _duration.Offset: r""" Gets start offset and checks to see whether indicator leaks to the right. This is either the wrapper's synthetic offset (if set); or the START offset of the wrapper's component (if indicator DOES NOT leak); or else the STOP offset of the wrapper's component (if indicator DOES leak). .. container:: example Start- and stop-text-spans attach to the same leaf. But stop-text-span leaks to the right: >>> voice = abjad.Voice("c'2 d'2") >>> start_text_span = abjad.StartTextSpan() >>> abjad.attach(start_text_span, voice[0]) >>> stop_text_span = abjad.StopTextSpan(leak=True) >>> abjad.attach(stop_text_span, voice[0]) >>> abjad.show(voice) # doctest: +SKIP >>> string = abjad.lilypond(voice) >>> print(string) \new Voice { c'2 \startTextSpan <> \stopTextSpan d'2 } Start offset and leaked start offset are the same for start-text-span: >>> wrapper = abjad.get.wrapper(voice[0], abjad.StartTextSpan) >>> wrapper.start_offset(), wrapper.leaked_start_offset() (Offset((0, 1)), Offset((0, 1))) Start offset and leaked start offset differ for stop-text-span: >>> wrapper = abjad.get.wrapper(voice[0], abjad.StopTextSpan) >>> wrapper.start_offset(), wrapper.leaked_start_offset() (Offset((0, 1)), Offset((1, 2))) """ synthetic_offset = self.synthetic_offset() if synthetic_offset is not None: return synthetic_offset indicator = self.unbundle_indicator() component = self.component() assert isinstance(component, _score.Component) if not getattr(indicator, "leak", False): return component._get_timespan().start_offset else: return component._get_timespan().stop_offset
[docs] def site_adjusted_start_offset(self) -> _duration.Offset: r""" Gets site-adjusted start offset. Indicators with site equal to ``absolute_after``, ``after`` or ``closing`` give a site-adjusted start offset equal to the stop offset of the wrapper's component. Indicators with any other site give a site-adjusted start offset equal to the start offset of the wrapper's component. This is the usual case, and means that site-adjusted start offset equals vanilla start offset. But if ``synthetic_offset`` is set then ``synthetic_offset`` is returned directly without examining the format site at all. .. container:: example >>> staff = abjad.Staff("c'4") >>> abjad.attach(abjad.Ottava(-1, site="before"), staff[0]) >>> abjad.attach(abjad.Ottava(0, site="after"), staff[0]) >>> for wrapper in abjad.get.wrappers(staff[0], abjad.Ottava): ... wrapper.indicator(), wrapper.site_adjusted_start_offset() (Ottava(n=-1, site='before'), Offset((0, 1))) (Ottava(n=0, site='after'), Offset((1, 4))) """ synthetic_offset = self.synthetic_offset() if synthetic_offset is not None: return synthetic_offset site = getattr(self.unbundle_indicator(), "site", "before") component = self.component() assert component is not None if site in ("absolute_after", "after", "closing"): return component._get_timespan().stop_offset else: return component._get_timespan().start_offset
[docs] def start_offset(self) -> _duration.Offset: """ Gets start offset. This is either the wrapper's synthetic offset or the start offset of the wrapper's component. """ synthetic_offset = self.synthetic_offset() if synthetic_offset is not None: return synthetic_offset component = self.component() assert isinstance(component, _score.Component) return component._get_timespan().start_offset
[docs] def synthetic_offset(self) -> _duration.Offset | None: """ Gets synthetic offset. """ return self._synthetic_offset
[docs] def tag(self) -> _tag.Tag: """ Gets wrapper tag. """ return self._tag
[docs] def set_tag(self, argument: _tag.Tag) -> None: """ Sets wrapper tag. """ assert isinstance(argument, _tag.Tag), repr(argument) self._tag = argument
[docs] def unbundle_indicator(self) -> typing.Any: """ Unbundles indicator. """ if self.is_bundled(): indicator = self.indicator().indicator else: indicator = self.indicator() return indicator