"""
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