From 47daeddfbba703a90de3bf43609c145c60837f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trevor=20Ba=C4=8Da?= Date: Fri, 19 Apr 2024 14:56:03 -0400 Subject: [PATCH] NEW. Added abjad.AfterGraceContainer.fraction property. (#1583) voice = abjad.Voice("c'4 d'4 e'4 f'4") notes = [abjad.Note("c'16"), abjad.Note("d'16")] after_grace_container = abjad.AfterGraceContainer(notes, fraction=(15, 16)) abjad.attach(after_grace_container, voice[1]) string = abjad.lilypond(voice) print(string) \new Voice { c'4 \afterGrace 15/16 d'4 { c'16 d'16 } e'4 f'4 } Closes #1505. FIXED. \afterGrace + \pitchedTrill contention. Closes #1582. --- .github/workflows/main.yml | 1 + Makefile | 6 +- abjad/score.py | 170 +++++++++++++++++++----------- setup.py | 2 +- tests/test_AfterGraceContainer.py | 33 ++++++ 5 files changed, 145 insertions(+), 67 deletions(-) create mode 100644 tests/test_AfterGraceContainer.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8344fb0d55..5c0b43cdad 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,6 +46,7 @@ jobs: run: | export PATH=/tmp/lilypond-2.25.3/bin:/home/runner/bin:$PATH export PYTHONUNBUFFERED=TRUE + pip install defusedxml pip install -e .[dev] # echo the configuration file path to debug read-only-filesystem handling python -c "import abjad; print(abjad.Configuration().configuration_file_path)" diff --git a/Makefile b/Makefile index d3fe1965d6..20084fd6cc 100644 --- a/Makefile +++ b/Makefile @@ -80,7 +80,7 @@ mypy: project = abjad pytest: - pytest tests + pytest abjad tests pytest-coverage: rm -Rf htmlcov/ @@ -88,10 +88,10 @@ pytest-coverage: --cov-config=.coveragerc \ --cov-report=html \ --cov=${project} \ - tests + abjad tests pytest-x: - pytest -x tests + pytest -x abjad tests reformat: make black-reformat diff --git a/abjad/score.py b/abjad/score.py index 0b67e85fd7..26e4150100 100644 --- a/abjad/score.py +++ b/abjad/score.py @@ -583,26 +583,44 @@ def _format_opening_site(self, contributions): if strings: result.append(f"% {_contributions.Types.COMMANDS.name}:") result.extend(strings) - strings = contributions.alphabetize(contributions.before.pitched_trill) - if strings: - result.append(f"% {_contributions.Types.PITCHED_TRILL.name}:") - result.extend(strings) - # IMPORTANT: LilyPond \pitchedTrill must appear immediately before leaf! - # IMPORTANT: LilyPond \afterGrace must appear immediately before leaf! - # TODO: figure out \pitchedTrill, \afterGrace ordering if self._after_grace_container is not None: assert not self._is_followed_by_after_grace_container() - result.append(r"\afterGrace") + string = r"\afterGrace" + if self._after_grace_container.fraction is not None: + n, d = self._after_grace_container.fraction + string = f"{string} {n}/{d}" + result.append(string) if self._is_followed_by_after_grace_container(): assert self._after_grace_container is None, repr( self._after_grace_container ) - result.append(r"\afterGrace") + string = r"\afterGrace" + container = self._get_following_after_grace_container() + if container.fraction is not None: + n, d = container.fraction + string = f"{string} {n}/{d}" + result.append(string) + strings = contributions.alphabetize(contributions.before.pitched_trill) + if strings: + result.append(f"% {_contributions.Types.PITCHED_TRILL.name}:") + result.extend(strings) return result def _get_compact_representation(self): return f"({self._get_formatted_duration()})" + def _get_following_after_grace_container(self): + if self._parent is not None: + index = self._parent.index(self) + try: + component = self._parent[index + 1] + except IndexError: + component = None + if isinstance(component, IndependentAfterGraceContainer): + return component + else: + return False + def _get_formatted_duration(self): strings = [] strings.append(self.written_duration.lilypond_duration_string) @@ -1830,14 +1848,9 @@ class AfterGraceContainer(Container): .. container:: example - After grace notes: - >>> voice = abjad.Voice("c'4 d'4 e'4 f'4") - >>> string = '#(define afterGraceFraction (cons 15 16))' - >>> literal = abjad.LilyPondLiteral(string, site="before") - >>> abjad.attach(literal, voice[0]) >>> notes = [abjad.Note("c'16"), abjad.Note("d'16")] - >>> after_grace_container = abjad.AfterGraceContainer(notes) + >>> after_grace_container = abjad.AfterGraceContainer(notes, fraction=(15, 16)) >>> abjad.attach(after_grace_container, voice[1]) >>> abjad.show(voice) # doctest: +SKIP @@ -1847,9 +1860,8 @@ class AfterGraceContainer(Container): >>> print(string) \new Voice { - #(define afterGraceFraction (cons 15 16)) c'4 - \afterGrace + \afterGrace 15/16 d'4 { c'16 @@ -1859,19 +1871,14 @@ class AfterGraceContainer(Container): f'4 } - LilyPond positions after grace notes at a point 3/4 of the way - after the note they follow. The resulting spacing is usually too - loose. - - Customize ``afterGraceFraction`` as shown above. + LilyPond positions after grace notes at a point 3/4 of the way after + the note they follow. The resulting spacing is usually too loose. + Customize ``fraction`` as shown here. After grace notes are played in the last moments of duration of the note they follow. - Use after grace notes when you need to end a piece of music with grace - notes. - - Fill grace containers with notes, rests or chords. + Fill after grace containers with notes, rests or chords. Attach after grace containers to notes, rests or chords. @@ -1881,11 +1888,8 @@ class AfterGraceContainer(Container): articulations and markup: >>> voice = abjad.Voice("c'4 d'4 e'4 f'4") - >>> string = '#(define afterGraceFraction (cons 15 16))' - >>> literal = abjad.LilyPondLiteral(string, site="before") - >>> abjad.attach(literal, voice[0]) - >>> after_grace_container = abjad.AfterGraceContainer("c'16 d'16") - >>> abjad.attach(after_grace_container, voice[1]) + >>> container = abjad.AfterGraceContainer("c'16 d'16", fraction=(15, 16)) + >>> abjad.attach(container, voice[1]) >>> leaves = abjad.select.leaves(voice, grace=None) >>> markup = abjad.Markup(r'\markup Allegro') >>> abjad.attach(markup, leaves[1], direction=abjad.UP) @@ -1898,9 +1902,8 @@ class AfterGraceContainer(Container): >>> print(string) \new Voice { - #(define afterGraceFraction (cons 15 16)) c'4 - \afterGrace + \afterGrace 15/16 d'4 - \staccato ^ \markup Allegro @@ -1919,11 +1922,8 @@ class AfterGraceContainer(Container): appear lexically after the ``\override`` command: >>> voice = abjad.Voice("c'4 4 e'4 f'4") - >>> string = '#(define afterGraceFraction (cons 15 16))' - >>> literal = abjad.LilyPondLiteral(string, site="before") - >>> abjad.attach(literal, voice[0]) - >>> after_grace_container = abjad.AfterGraceContainer("c'16 d'16") - >>> abjad.attach(after_grace_container, voice[1]) + >>> container = abjad.AfterGraceContainer("c'16 d'16", fraction=(15, 16)) + >>> abjad.attach(container, voice[1]) >>> abjad.override(voice[1]).NoteHead.color = "#red" >>> abjad.show(voice) # doctest: +SKIP @@ -1933,10 +1933,9 @@ class AfterGraceContainer(Container): >>> print(string) \new Voice { - #(define afterGraceFraction (cons 15 16)) c'4 \once \override NoteHead.color = #red - \afterGrace + \afterGrace 15/16 4 { c'16 @@ -1952,26 +1951,36 @@ class AfterGraceContainer(Container): __documentation_section__ = "Containers" - __slots__ = ("_main_leaf",) + __slots__ = ("_fraction", "_main_leaf") ### INITIALIZER ### def __init__( - self, components=None, *, language: str = "english", tag: _tag.Tag | None = None + self, + components=None, + *, + fraction: tuple[int, int] | None = None, + language: str = "english", + tag: _tag.Tag | None = None, ) -> None: - # _main_leaf must be initialized before container initialization + # NOTE: _main_leaf must be initialized before container initialization self._main_leaf = None Container.__init__(self, components, language=language, tag=tag) + self.fraction = fraction ### SPECIAL METHODS ### - def __getnewargs__(self): + def __getnewargs__(self) -> tuple[tuple[int, int] | None]: """ Gets new after grace container arguments. - Returns tuple of single empty list. + .. container:: example + + >>> abjad.AfterGraceContainer("d'8", fraction=(15, 16)).__getnewargs__() + ((15, 16),) + """ - return ([],) + return (self.fraction,) ### PRIVATE METHODS ### @@ -1993,6 +2002,23 @@ def _format_open_brackets_site(self, contributions): result.extend(["{"]) return result + @property + def fraction(self) -> tuple[int, int] | None: + r""" + Gets LilyPond `\afterGraceFraction`. + """ + return self._fraction + + @fraction.setter + def fraction(self, fraction: tuple[int, int] | None): + if fraction is not None: + assert isinstance(fraction, tuple), repr(fraction) + assert len(fraction) == 2, repr(fraction) + assert isinstance(fraction[0], int), repr(fraction) + assert isinstance(fraction[0], int), repr(fraction) + assert isinstance(fraction[1], int), repr(fraction) + self._fraction = fraction + class BeforeGraceContainer(Container): r""" @@ -3245,12 +3271,9 @@ class IndependentAfterGraceContainer(Container): After grace notes: >>> voice = abjad.Voice("c'4 d'4 e'4 f'4") - >>> string = '#(define afterGraceFraction (cons 15 16))' - >>> literal = abjad.LilyPondLiteral(string, site="before") - >>> abjad.attach(literal, voice[0]) >>> notes = [abjad.Note("c'16"), abjad.Note("d'16")] - >>> after_grace_container = abjad.IndependentAfterGraceContainer(notes) - >>> voice.insert(2, after_grace_container) + >>> container = abjad.IndependentAfterGraceContainer(notes, fraction=(15, 16)) + >>> voice.insert(2, container) >>> abjad.show(voice) # doctest: +SKIP .. docs:: @@ -3259,9 +3282,8 @@ class IndependentAfterGraceContainer(Container): >>> print(string) \new Voice { - #(define afterGraceFraction (cons 15 16)) c'4 - \afterGrace + \afterGrace 15/16 d'4 { c'16 @@ -3273,25 +3295,32 @@ class IndependentAfterGraceContainer(Container): LilyPond positions after grace notes at a point 3/4 of the way after the note they follow. The resulting spacing is usually too - loose. - - Customize ``afterGraceFraction`` as shown above. + loose. Customize ``fraction`` as shown above. After grace notes are played in the last moments of the duration of the note they follow. - Use after grace notes when you need to end a piece of music with grace - notes. - Fill grace containers with notes, rests or chords. - """ ### CLASS VARIABLES ### __documentation_section__ = "Containers" - ### SPECIAL METHODS ### + __slots__ = ("_fraction",) + + ### INITIALIZER ### + + def __init__( + self, + components=None, + *, + fraction: tuple[int, int] | None = None, + language: str = "english", + tag: _tag.Tag | None = None, + ) -> None: + Container.__init__(self, components, language=language, tag=tag) + self.fraction = fraction def __getnewargs__(self): """ @@ -3301,8 +3330,6 @@ def __getnewargs__(self): """ return ([],) - ### PRIVATE METHODS ### - def _format_open_brackets_site(self, contributions): result = [] result.extend(["{"]) @@ -3311,6 +3338,23 @@ def _format_open_brackets_site(self, contributions): def _get_preprolated_duration(self): return _duration.Duration(0) + @property + def fraction(self) -> tuple[int, int] | None: + r""" + Gets LilyPond `\afterGraceFraction`. + """ + return self._fraction + + @fraction.setter + def fraction(self, fraction: tuple[int, int] | None): + if fraction is not None: + assert isinstance(fraction, tuple), repr(fraction) + assert len(fraction) == 2, repr(fraction) + assert isinstance(fraction[0], int), repr(fraction) + assert isinstance(fraction[0], int), repr(fraction) + assert isinstance(fraction[1], int), repr(fraction) + self._fraction = fraction + class MultimeasureRest(Leaf): r""" diff --git a/setup.py b/setup.py index 76bdf7f2c9..e9957a377d 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ def check_python_version(abjad_version): "flake8>=6.1.0", "isort>=5.12.0", "mypy>=1.4.1", - "pytest>=7.4.0", + "pytest>=8.1.1", "pytest-cov>=4.1.0", "pytest-helpers-namespace>=2021.12.29", "sphinx-autodoc-typehints>=1.22.0", diff --git a/tests/test_AfterGraceContainer.py b/tests/test_AfterGraceContainer.py new file mode 100644 index 0000000000..1fa4b664f0 --- /dev/null +++ b/tests/test_AfterGraceContainer.py @@ -0,0 +1,33 @@ +import abjad + + +def test_AfterGraceContainer_01(): + r""" + REGRESSION. LilyPond \afterGrace must lexically precede LilyPond \pitchedTrill. + """ + + voice = abjad.Voice("c'1") + container = abjad.IndependentAfterGraceContainer("e'8") + voice.append(container) + voice.append("r4") + start_trill_span = abjad.StartTrillSpan(pitch=abjad.NamedPitch("D4")) + abjad.attach(start_trill_span, voice[0]) + stop_trill_span = abjad.StopTrillSpan() + abjad.attach(stop_trill_span, voice[-1]) + + assert abjad.lilypond(voice) == abjad.string.normalize( + r""" + \new Voice + { + \afterGrace + \pitchedTrill + c'1 + \startTrillSpan d' + { + e'8 + } + r4 + \stopTrillSpan + } + """ + )