Source code for abjad.wrapper

"""
Wrapper.
"""

import copy
import enum
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 score as _score
from . import tag as _tag
from . import tweaks as _tweaks


[docs] class Wrapper: r""" Wrapper. .. container:: example Attaches articulation to note and makes wrapper: >>> component = abjad.Note("c'4") >>> articulation = abjad.Articulation("accent") >>> abjad.attach(articulation, component, direction=abjad.UP) >>> abjad.get.wrapper(component) Wrapper(annotation=None, context=None, deactivate=False, direction=<Vertical.UP: 1>, indicator=Articulation(name='accent'), synthetic_offset=None, tag=Tag(string='')) .. container:: example Duplicate indicator warnings take two forms. >>> voice_1 = abjad.Voice("c''4 d'' e'' f''", name="VoiceI") >>> voice_2 = abjad.Voice("c'4 d' e' f'", name="VoiceII") >>> staff = abjad.Staff([voice_1, voice_2], simultaneous=True) >>> abjad.attach(abjad.Clef("alto"), voice_2[0]) >>> abjad.show(staff) # doctest: +SKIP .. docs:: >>> string = abjad.lilypond(staff) >>> print(string) \new Staff << \context Voice = "VoiceI" { c''4 d''4 e''4 f''4 } \context Voice = "VoiceII" { \clef "alto" c'4 d'4 e'4 f'4 } >> First form when attempting to attach a contexted indicator to a leaf that already carries a contexted indicator of the same type: >>> # abjad.attach(abjad.Clef("treble"), voice_2[0]) Second form when attempting to attach a contexted indicator to a leaf governed by some other component carrying a contexted indicator of the same type. >>> # abjad.attach(abjad.Clef("treble"), voice_1[0]) """ __slots__ = ( "_annotation", "_component", "_context", "_deactivate", "_direction", "_effective_context", "_indicator", "_synthetic_offset", "_tag", ) def __init__( self, annotation: str | enum.Enum | None = None, # TODO: move check_duplicate_indicator to _unsafe_attach() check_duplicate_indicator: bool = False, component: _score.Component | None = None, context: str | None = None, deactivate: bool = False, direction: _enums.Vertical | None = None, indicator: typing.Any = None, synthetic_offset: _duration.Offset | None = None, tag: _tag.Tag = _tag.Tag(), ) -> None: assert not isinstance(indicator, type(self)), repr(indicator) if annotation is not None: assert isinstance(annotation, str | enum.Enum), repr(annotation) self._annotation = annotation if component is not None: assert isinstance(component, _score.Component), repr(component) self._component = component deactivate = bool(deactivate) if context is not None: assert isinstance(context, str), repr(context) self._context = context if deactivate is not None: deactivate = bool(deactivate) self._deactivate = deactivate self._direction = direction self._effective_context = None self._indicator = indicator self._synthetic_offset: _duration.Offset | None if synthetic_offset is not None: assert isinstance(synthetic_offset, _duration.Offset), repr( synthetic_offset ) self._synthetic_offset = _duration.Offset(synthetic_offset) else: self._synthetic_offset = synthetic_offset if tag is not None: assert isinstance(tag, str | _tag.Tag) assert isinstance(tag, _tag.Tag), repr(tag) self._tag = tag if component is not None: self._bind_component( component, check_duplicate_indicator=check_duplicate_indicator ) def __copy__(self, *arguments) -> "Wrapper": r""" Copies wrapper. Preserves annotation flag: .. container:: example >>> old_staff = abjad.Staff("c'4 d'4 e'4 f'4") >>> abjad.annotate(old_staff[0], "bow_direction", abjad.DOWN) >>> string = abjad.lilypond(old_staff) >>> print(string) \new Staff { c'4 d'4 e'4 f'4 } >>> leaf = old_staff[0] >>> abjad.get.annotation(leaf, "bow_direction") <Vertical.DOWN: -1> >>> new_staff = abjad.mutate.copy(old_staff) >>> string = abjad.lilypond(new_staff) >>> print(string) \new Staff { c'4 d'4 e'4 f'4 } >>> leaf = new_staff[0] >>> abjad.get.annotation(leaf, "bow_direction") <Vertical.DOWN: -1> .. container:: example Preserves tag: >>> old_staff = abjad.Staff("c'4 d'4 e'4 f'4") >>> clef = abjad.Clef("alto") >>> abjad.attach(clef, old_staff[0], tag=abjad.Tag("RED:M1")) >>> string = abjad.lilypond(old_staff, tags=True) >>> print(string) \new Staff { %! M1 %! RED \clef "alto" c'4 d'4 e'4 f'4 } >>> leaf = old_staff[0] >>> abjad.get.wrapper(leaf) Wrapper(annotation=None, context='Staff', deactivate=False, direction=None, indicator=Clef(name='alto', hide=False), synthetic_offset=None, tag=Tag(string='RED:M1')) >>> new_staff = abjad.mutate.copy(old_staff) >>> string = abjad.lilypond(new_staff, tags=True) >>> print(string) \new Staff { %! M1 %! RED \clef "alto" c'4 d'4 e'4 f'4 } >>> leaf = new_staff[0] >>> abjad.get.wrapper(leaf) Wrapper(annotation=None, context='Staff', deactivate=False, direction=None, indicator=Clef(name='alto', hide=False), synthetic_offset=None, tag=Tag(string='RED:M1')) .. container:: example Preserves deactivate flag: >>> old_staff = abjad.Staff("c'4 d'4 e'4 f'4") >>> abjad.attach( ... abjad.Clef("alto"), ... old_staff[0], ... deactivate=True, ... tag=abjad.Tag("RED:M1"), ... ) >>> string = abjad.lilypond(old_staff, tags=True) >>> print(string) \new Staff { %! M1 %! RED %@% \clef "alto" c'4 d'4 e'4 f'4 } >>> leaf = old_staff[0] >>> abjad.get.wrapper(leaf) Wrapper(annotation=None, context='Staff', deactivate=True, direction=None, indicator=Clef(name='alto', hide=False), synthetic_offset=None, tag=Tag(string='RED:M1')) >>> new_staff = abjad.mutate.copy(old_staff) >>> string = abjad.lilypond(new_staff, tags=True) >>> print(string) \new Staff { %! M1 %! RED %@% \clef "alto" c'4 d'4 e'4 f'4 } >>> leaf = new_staff[0] >>> abjad.get.wrapper(leaf) Wrapper(annotation=None, context='Staff', deactivate=True, direction=None, indicator=Clef(name='alto', hide=False), synthetic_offset=None, tag=Tag(string='RED:M1')) Copies all properties except component. Copy operations must supply component after wrapper copy. """ new = type(self)( annotation=self.annotation, component=None, context=self.context, deactivate=self.deactivate, direction=self.direction, indicator=copy.copy(self.indicator), synthetic_offset=self.synthetic_offset, tag=self.tag, ) return new def __eq__(self, argument) -> bool: """ Is true when self equals ``argument``. """ if not isinstance(argument, Wrapper): return False if self.annotation != argument.annotation: return False if self.component != argument.component: return False if self.context != argument.context: return False if self.deactivate != argument.deactivate: 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: """ Hashes wrapper. """ return hash(self.__class__.__name__ + str(self)) def __repr__(self) -> str: """ Gets repr. """ parameters = f""" annotation={self.annotation!r}, context={self.context!r}, deactivate={self.deactivate!r}, direction={self.direction!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, check_duplicate_indicator=False): if isinstance(self.indicator, _tweaks.Bundle): indicator = self.indicator.indicator else: indicator = self.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): 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() if isinstance(self.indicator, _tweaks.Bundle): indicator = self.indicator.indicator else: indicator = self.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): if self.deactivate is True: return if isinstance(self.indicator, _tweaks.Bundle): indicator = self.indicator.indicator else: indicator = self.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 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 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 ..." 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): self._unbind_component() self._unbind_effective_context() return self @staticmethod def _find_correct_effective_context(component, context): if context is None: return None abjad = importlib.import_module("abjad") context = getattr(abjad, context, context) candidate = None parentage = component._get_parentage() if isinstance(context, type): for component in parentage: if not hasattr(component, "_lilypond_type"): continue if isinstance(component, context): candidate = component break elif isinstance(context, str): for component in parentage: if not hasattr(component, "_lilypond_type"): continue if component.name == context or component.lilypond_type == context: candidate = component break else: raise TypeError("must be context or string: {context!r}.") if candidate.__class__.__name__ == "Voice": for component in reversed(parentage): if not component.__class__.__name__ == "Voice": continue if component.name == candidate.name: candidate = component break return candidate def _get_effective_context(self): if self.component is not None: _updatelib._update_now(self.component, indicators=True) return self._effective_context def _unbind_component(self): if self._component is not None and id(self) in [ id(_) for _ in self._component._wrappers ]: self._component._wrappers.remove(self) self._component = None def _unbind_effective_context(self): 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): correct_effective_context = self._find_correct_effective_context( self.component, self.context ) 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) @property def annotation(self) -> str | enum.Enum | None: """ Gets wrapper annotation. .. container:: example >>> note = abjad.Note("c'4") >>> articulation = abjad.Articulation("accent") >>> abjad.attach(articulation, note, direction=abjad.UP) >>> wrapper = abjad.get.wrapper(note) >>> wrapper.annotation is None True >>> note = abjad.Note("c'4") >>> articulation = abjad.Articulation("accent") >>> abjad.annotate(note, "foo", articulation) >>> abjad.get.annotation(note, "foo") Articulation(name='accent') """ return self._annotation @property def component(self) -> _score.Component | None: """ Gets start component. """ return self._component @property def context(self) -> str | None: """ Gets context (name). """ return self._context @property def deactivate(self) -> bool: """ Is true when wrapper deactivates tag. """ assert self._deactivate in (True, False, None) return self._deactivate @deactivate.setter def deactivate(self, argument): assert argument in (True, False, None) self._deactivate = argument @property def direction(self): """ Gets direction of indicator. """ return self._direction @property def indicator(self) -> typing.Any: """ Gets indicator. """ return self._indicator @property 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))) """ if self._synthetic_offset is not None: return self._synthetic_offset if isinstance(self.indicator, _tweaks.Bundle): indicator = self.indicator.indicator else: indicator = self.indicator assert isinstance(self.component, _score.Component) if not getattr(indicator, "leak", False): return self.component._get_timespan().start_offset else: return self.component._get_timespan().stop_offset @property def site_adjusted_start_offset(self) -> _duration.Offset: r""" Gets site-adjusted start offset. .. container:: example 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. >>> 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))) """ if self.synthetic_offset is not None: return self.synthetic_offset site = getattr(self.unbundle_indicator(), "site", "before") assert self.component is not None if site in ("absolute_after", "after", "closing"): return self.component._get_timespan().stop_offset else: return self.component._get_timespan().start_offset @property 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. """ if self.synthetic_offset is not None: return self.synthetic_offset assert isinstance(self.component, _score.Component) return self.component._get_timespan().start_offset @property def synthetic_offset(self) -> _duration.Offset | None: """ Gets synthetic offset. """ return self._synthetic_offset @property def tag(self) -> _tag.Tag: """ Gets and sets tag. """ assert isinstance(self._tag, _tag.Tag), repr(self._tag) return self._tag @tag.setter def tag(self, argument): if not isinstance(argument, _tag.Tag): raise Exception(f"must be tag: {argument!r}.") self._tag = argument
[docs] def bundled(self): """ Is true when indicator is bundled. """ return isinstance(self._indicator, _tweaks.Bundle)
[docs] def get_item(self): """ Gets indicator or bundled indicator. """ return self._indicator
[docs] def unbundle_indicator(self): """ Unbundles indicator. """ if isinstance(self.indicator, _tweaks.Bundle): return self.indicator.indicator return self.indicator