From 2917c0da851debefeb64cd0a7c95d5b91165d736 Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Tue, 29 Apr 2025 00:36:17 +0200 Subject: [PATCH 01/19] Add unit tests for paragraph bullet --- tests/text/test_text.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 3a1a7a0bb..6a3f9880f 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -870,6 +870,11 @@ def it_knows_what_text_it_contains(self, text_get_fixture): assert text == expected_value assert isinstance(text, str) + def it_can_change_its_bullet(self, bullet_set_fixture): + paragraph, new_value, expected_xml = bullet_set_fixture + paragraph.bullet = new_value + assert paragraph._element.xml == expected_xml + @pytest.mark.parametrize( ("p_cxml", "value", "expected_cxml"), [ @@ -1119,6 +1124,24 @@ def text_get_fixture(self, request): p = element(p_cxml) return p, expected_value + + @pytest.fixture( + params=[ + ("a:p", 'x', 'a:p/a:pPr/a:buChar{char=x}'), + ("a:p", True, u'a:p/a:pPr/a:buChar{char=\u2022}'), + ("a:p", False, 'a:p/a:pPr/a:buNone'), + ("a:p", None, 'a:p/a:pPr'), + ("a:p/a:pPr/a:buNone", None, 'a:p/a:pPr'), + (u'a:p/a:pPr/a:buChar', None, 'a:p/a:pPr'), + ] + ) + def bullet_set_fixture(self, request): + p_cxml, new_value, expected_p_cxml = request.param + paragraph = _Paragraph(element(p_cxml), None) + expected_xml = xml(expected_p_cxml) + return paragraph, new_value, expected_xml + + # fixture components ----------------------------------- @pytest.fixture From 503df0e63eb7a0f0e0c80679f7fae460c3d43798 Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Tue, 29 Apr 2025 00:39:00 +0200 Subject: [PATCH 02/19] Implement bullet getter and setter --- src/pptx/oxml/__init__.py | 4 +++ src/pptx/oxml/text.py | 55 +++++++++++++++++++++++++++++++++++++++ src/pptx/text/text.py | 12 +++++++++ tests/unitutil/cxml.py | 2 +- 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/pptx/oxml/__init__.py b/src/pptx/oxml/__init__.py index 21afaa921..eea1684fd 100644 --- a/src/pptx/oxml/__init__.py +++ b/src/pptx/oxml/__init__.py @@ -457,6 +457,8 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]): CT_TextSpacing, CT_TextSpacingPercent, CT_TextSpacingPoint, + CT_TextNoBullet, + CT_TextCharBullet, ) register_element_cls("a:bodyPr", CT_TextBodyProperties) @@ -476,6 +478,8 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]): register_element_cls("a:spcBef", CT_TextSpacing) register_element_cls("a:spcPct", CT_TextSpacingPercent) register_element_cls("a:spcPts", CT_TextSpacingPoint) +register_element_cls('a:buNone', CT_TextNoBullet) +register_element_cls('a:buChar', CT_TextCharBullet) register_element_cls("a:txBody", CT_TextBody) register_element_cls("c:txPr", CT_TextBody) register_element_cls("p:txBody", CT_TextBody) diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index 0f9ecc152..9447e350f 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -26,6 +26,7 @@ ST_TextTypeface, ST_TextWrappingType, XsdBoolean, + XsdString, ) from pptx.oxml.xmlchemy import ( BaseOxmlElement, @@ -466,9 +467,13 @@ class CT_TextParagraphProperties(BaseOxmlElement): _add_lnSpc: Callable[[], CT_TextSpacing] _add_spcAft: Callable[[], CT_TextSpacing] _add_spcBef: Callable[[], CT_TextSpacing] + _add_buNone: Callable[[], CT_TextNoBullet] + _add_buChar: Callable[[], CT_TextCharBullet] _remove_lnSpc: Callable[[], None] _remove_spcAft: Callable[[], None] _remove_spcBef: Callable[[], None] + _remove_buNone: Callable[[], None] + _remove_buChar: Callable[[], None] _tag_seq = ( "a:lnSpc", @@ -501,6 +506,12 @@ class CT_TextParagraphProperties(BaseOxmlElement): defRPr: CT_TextCharacterProperties | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "a:defRPr", successors=_tag_seq[16:] ) + buNone: CT_TextNoBullet | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:buNone", successors=_tag_seq[11:] + ) + buChar: CT_TextCharBullet | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:buChar", successors=_tag_seq[13:] + ) lvl: int = OptionalAttribute( # pyright: ignore[reportAssignmentType] "lvl", ST_TextIndentLevelType, default=0 ) @@ -534,6 +545,33 @@ def line_spacing(self, value: float | Length | None): else: self._add_lnSpc().set_spcPct(value) + @property + def bullet(self) -> bool | str | None: + buNone = self.buNone + buChar = self.buChar + if buNone is not None: + return False + if buChar is not None: + return buChar.char + + @bullet.setter + def bullet(self, value: bool | str | None): + self._remove_buNone() + self._remove_buChar() + if value is None: + return + if isinstance(value, bool): + if value: + buChar = self._add_buChar() + buChar.char = u"\u2022" + else: + self._add_buNone() + + else: + buChar = self._add_buChar() + buChar.char = value + + @property def space_after(self) -> Length | None: """The EMU equivalent of the centipoints value in `./a:spcAft/a:spcPts/@val`.""" @@ -616,3 +654,20 @@ class CT_TextSpacingPoint(BaseOxmlElement): val: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType] "val", ST_TextSpacingPoint ) + + +class CT_TextNoBullet(BaseOxmlElement): + """ + element, specifying that a paragraph should not be bulleted. + """ + pass + + +class CT_TextCharBullet(BaseOxmlElement): + """ + element, specifying that a paragraph should have a character bullet. + """ + + char: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "char", XsdString + ) \ No newline at end of file diff --git a/src/pptx/text/text.py b/src/pptx/text/text.py index e139410c2..02c44cdc8 100644 --- a/src/pptx/text/text.py +++ b/src/pptx/text/text.py @@ -547,6 +547,18 @@ def line_spacing(self, value: int | float | Length | None): pPr = self._p.get_or_add_pPr() pPr.line_spacing = value + @property + def bullet(self): + pPr = self._p.pPr + if pPr is None: + return None + return pPr.bullet + + @bullet.setter + def bullet(self, value: bool | str | None): + pPr = self._p.get_or_add_pPr() + pPr.bullet = value + @property def runs(self) -> tuple[_Run, ...]: """Sequence of runs in this paragraph.""" diff --git a/tests/unitutil/cxml.py b/tests/unitutil/cxml.py index 79e217c20..038cbe021 100644 --- a/tests/unitutil/cxml.py +++ b/tests/unitutil/cxml.py @@ -252,7 +252,7 @@ def grammar(): # np:attr_name=attr_val ---------------------- attr_name = Word(alphas + ":") - attr_val = Word(alphanums + " %-./:_") + attr_val = Word(alphanums + u" %-./:_\u2022") attr_def = Group(attr_name + equal + attr_val) attr_list = open_brace + delimitedList(attr_def) + close_brace From f0bcef9cca09af9163cb4d656e8d44ec69eb2182 Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Tue, 29 Apr 2025 00:39:19 +0200 Subject: [PATCH 03/19] Test paragraph bullet with behave --- features/steps/text.py | 18 ++++++++++++++++++ features/txt-paragraph.feature | 13 +++++++++++++ 2 files changed, 31 insertions(+) diff --git a/features/steps/text.py b/features/steps/text.py index 5c3692b5d..1096f842e 100644 --- a/features/steps/text.py +++ b/features/steps/text.py @@ -138,6 +138,18 @@ def when_set_hyperlink_address(context): hlink.address = context.address +@when("I assign paragraph.bullet = {value_str}") +def when_I_assign_value_to_paragraph_bullet(context, value_str): + value = { + "x": "x", + "True": True, + "False": False, + "None": None, + }[value_str] + paragraph = context.paragraph + paragraph.bullet = value + + # then ==================================================== @@ -246,3 +258,9 @@ def then_run_text_is_not_a_hyperlink(context): @then("the font name matches the typeface I set") def then_font_name_matches_typeface_I_set(context): assert context.font.name == "Verdana" + + +@then("paragraph.bullet == {value}") +def then_paragraph_bullet_is_value(context, value): + actual, expected = context.paragraph.bullet, eval(value) + assert actual == expected, 'paragraph.bullet == "%s"' % actual diff --git a/features/txt-paragraph.feature b/features/txt-paragraph.feature index e0799a792..cc969e36a 100644 --- a/features/txt-paragraph.feature +++ b/features/txt-paragraph.feature @@ -93,3 +93,16 @@ Feature: Change paragraph properties | "a\vb\vc" | "a\vb\vc" | | "a\nb\vc" | "a\vb\vc" | | "a\x1Bc" | "a_x001B_c" | + + + Scenario Outline: _Paragraph.bullet setter + Given a _Paragraph object as paragraph + When I assign paragraph.bullet = + Then paragraph.bullet == + + Examples: _Paragraph assigned bullet replacement cases + | value | expected-value | + | x | "x" | + | False | False | + | True | "\u2022" | + | None | None | From 46c338758b519fb2cb60a3aef97ebe83d04604cc Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Tue, 29 Apr 2025 16:24:12 +0200 Subject: [PATCH 04/19] Add unit tests and enums for numbered bullets --- src/pptx/enum/text.py | 50 +++++++++++++++++++++++++++++++++++++++++ tests/text/test_text.py | 28 +++++++++++++++++------ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/pptx/enum/text.py b/src/pptx/enum/text.py index db266a3c9..a13ff2986 100644 --- a/src/pptx/enum/text.py +++ b/src/pptx/enum/text.py @@ -228,3 +228,53 @@ class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum): PP_ALIGN = PP_PARAGRAPH_ALIGNMENT + + +class MSO_NUMBERED_BULLET_STYLE(BaseXmlEnum): + """ + MS API Name: `MsoNumberedBulletStyle` + https://learn.microsoft.com/sv-se/office/vba/api/office.msonumberedbulletstyle + """ + + BULLET_ALPHA_LC_PAREN_BOTH = (8, "alphaLCParenBoth", "Lowercase alphabetical bullet with opening and closing parentheses.") + BULLET_ALPHA_LC_PAREN_RIGHT = (9, "alphaLCParenRight", "Lowercase alphabetical bullet with closing parenthesis.") + BULLET_ALPHA_LC_PERIOD = (0, "alphaLCPeriod", "Lowercase alphabetical bullet with period.|") + BULLET_ALPHA_UC_PAREN_BOTH = (10, "alphaUCParenBoth", "Uppercase alphabetical bullet with opening and closing parentheses.") + BULLET_ALPHA_UC_PAREN_RIGHT = (11, "alphaUCParenRight", "Uppercase alphabetical bullet with closing parenthesis.") + BULLET_ALPHA_UC_PERIOD = (1, "alphaUCPeriod", "Uppercase alphabetical bullet with period.") + BULLET_ARABIC_ABJAD_DASH = (24, "arabicAbjadDash", "Arabic Abjad bullet with a dash.") + BULLET_ARABIC_ALPHA_DASH = (23, "arabicAlphaDash", "Arabic alphabetical bullet with a dash.") + BULLET_ARABIC_DB_PERIOD = (29, "arabicDBPeriod", "Arabic DB bullet with period.") + BULLET_ARABIC_DB_PLAIN = (28, "arabicDBPlain", "Plain Arabic DB bullet.") + BULLET_ARABIC_PAREN_BOTH = (12, "arabicParenBoth", "Arabic bullet with opening and closing parentheses.") + BULLET_ARABIC_PAREN_RIGHT = (2, "arabicParenRight", "Arabic bullet with closing parenthesis.") + BULLET_ARABIC_PERIOD = (3, "arabicPeriod", "Arabic bullet with period.") + BULLET_ARABIC_PLAIN = (13, "arabicPlain", "Plain Arabic bullet.") + BULLET_CIRCLE_NUM_DB_PLAIN = (18, "circleNumDBPlain", "Circled number bullet.") + BULLET_CIRCLE_NUM_WD_BLACK_PLAIN = (20, "circleNumWDBlackPlain", "Circled number WD black bullet.") + BULLET_CIRCLE_NUM_WD_WHITE_PLAIN = (19, "circleNumWDWhitePlain", "Circled number WD white bullet.") + BULLET_HEBREW_ALPHA_DASH = (25, "hebrewAlphaDash", "Hebrew alphabetical bullet with dash.") + BULLET_HINDI_ALPHA_1PERIOD = (40, "hindiAlpha1Period", "Hindi alphabetical bullet 1 with period.") + BULLET_HINDI_ALPHA_PERIOD = (36, "hindiAlphaPeriod", "Hindi alphabetical bullet with period.") + BULLET_HINDI_NUM_PAREN_RIGHT = (39, "hindiNumParenRight", "Hindi numbered bullet with closing parenthesis.") + BULLET_HINDI_NUM_PERIOD = (37, "hindiNumPeriod", "Hindi numbered bullet with period.") + BULLET_KANJI_KOREAN_PERIOD = (27, "kanjiKoreanPeriod", "Korean Kanji bullet with period.") + BULLET_KANJI_KOREAN_PLAIN = (26, "kanjiKoreanPlain", "Korean Kanji bullet.") + BULLET_KANJI_SIMP_CHIN_DB_PERIOD = (38, "kanjiSimpChinDBPeriod", "Simplified Chinese Kanji bulllet with period.") + BULLET_ROMAN_LC_PAREN_BOTH = (4, "romanLCParenBoth", "Lowercase roman bullet with opening and closing parentheses.") + BULLET_ROMAN_LC_PAREN_RIGHT = (5, "romanLCParenRight", "Lowercase roman bullet with closing parenthesis.") + BULLET_ROMAN_LC_PERIOD = (6, "romanLCPeriod", "Lowercase roman bullet with period.") + BULLET_ROMAN_UC_PAREN_BOTH = (14, "romanUCParenBoth", "Uppercase roman bullet with opening and closing parentheses.") + BULLET_ROMAN_UC_PAREN_RIGHT = (15, "romanUCParenRight", "Uppercase roman bullet with closing parenthesis.") + BULLET_ROMAN_UC_PERIOD = (7, "romanUCPeriod", "Uppercase roman bullet with period.") + BULLET_SIMP_CHIN_PERIOD = (17, "simpChinPeriod", "Simplified Chinese bulllet with period.") + BULLET_SIMP_CHIN_PLAIN = (16, "simpChinPlain", "Simplified Chinese bullet.") + BULLET_STYLE_MIXED = (-2, "styleMixed", "Return value only; indicates a combination of the other states. ") + BULLET_THAI_ALPHA_PAREN_BOTH = (32, "thaiAlphaParenBoth", "Thai alphabetical bullet with opening and closing parentheses.") + BULLET_THAI_ALPHA_PAREN_RIGHT = (31, "thaiAlphaParenRight", "Thai alphabetical bullet with closing parenthesis.") + BULLET_THAI_ALPHA_PERIOD = (30, "thaiAlphaPeriod", "Thai alphabetical bullet with period.") + BULLET_THAI_NUM_PAREN_BOTH = (35, "thaiNumParenBoth", "Thai numerical bullet with opening and closing parentheses.") + BULLET_THAI_NUM_PAREN_RIGHT = (34, "thaiNumParenRight", "Thai numerical bullet with closing parenthesis.") + BULLET_THAI_NUM_PERIOD = (33, "thaiNumPeriod", "Thai numerical bullet with period.") + BULLET_TRAD_CHIN_PERIOD = (22, "tradChinPeriod", "Traditional Chinese bulllet with period.") + BULLET_TRAD_CHIN_PLAIN = (21, "tradChinPlain", "Traditional Chinese bulllet.") \ No newline at end of file diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 6a3f9880f..9288d8ef6 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -11,7 +11,7 @@ from pptx.dml.color import ColorFormat from pptx.dml.fill import FillFormat from pptx.enum.lang import MSO_LANGUAGE_ID -from pptx.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE, MSO_UNDERLINE, PP_ALIGN +from pptx.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE, MSO_NUMBERED_BULLET_STYLE, MSO_UNDERLINE, PP_ALIGN from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.opc.package import XmlPart from pptx.shapes.autoshape import Shape @@ -1127,12 +1127,26 @@ def text_get_fixture(self, request): @pytest.fixture( params=[ - ("a:p", 'x', 'a:p/a:pPr/a:buChar{char=x}'), - ("a:p", True, u'a:p/a:pPr/a:buChar{char=\u2022}'), - ("a:p", False, 'a:p/a:pPr/a:buNone'), - ("a:p", None, 'a:p/a:pPr'), - ("a:p/a:pPr/a:buNone", None, 'a:p/a:pPr'), - (u'a:p/a:pPr/a:buChar', None, 'a:p/a:pPr'), + ("a:p", "x", "a:p/a:pPr/a:buChar{char=x}"), + ("a:p/a:pPr/a:buChar", "x", "a:p/a:pPr/a:buChar{char=x}"), + ("a:p/a:pPr/a:buNone", "x", "a:p/a:pPr/a:buChar{char=x}"), + ("a:p/a:pPr/a:buAutoNum", "x", "a:p/a:pPr/a:buChar{char=x}"), + ("a:p", True, u"a:p/a:pPr/a:buChar{char=\u2022}"), + ("a:p/a:pPr/a:buChar", True, u"a:p/a:pPr/a:buChar{char=\u2022}"), + ("a:p/a:pPr/a:buNone", True, u"a:p/a:pPr/a:buChar{char=\u2022}"), + ("a:p/a:pPr/a:buAutoNum", True, u"a:p/a:pPr/a:buChar{char=\u2022}"), + ("a:p", False, "a:p/a:pPr/a:buNone"), + ("a:p/a:pPr/a:buChar", False, "a:p/a:pPr/a:buNone"), + ("a:p/a:pPr/a:buNone", False, "a:p/a:pPr/a:buNone"), + ("a:p/a:pPr/a:buAutoNum", False, "a:p/a:pPr/a:buNone"), + ("a:p", None, "a:p/a:pPr"), + ("a:p/a:pPr/a:buNone", None, "a:p/a:pPr"), + ("a:p/a:pPr/a:buChar", None, "a:p/a:pPr"), + ("a:p/a:pPr/a:buAutoNum", None, "a:p/a:pPr"), + ("a:p", MSO_NUMBERED_BULLET_STYLE.BULLET_ROMAN_UC_PERIOD, "a:p/a:pPr/a:buAutoNum{type=romanUCPeriod}"), + ("a:p/a:pPr/a:buChar", MSO_NUMBERED_BULLET_STYLE.BULLET_ALPHA_LC_PERIOD, "a:p/a:pPr/a:buAutoNum{type=alphaLCPeriod}"), + ("a:p/a:pPr/a:buNone", MSO_NUMBERED_BULLET_STYLE.BULLET_ARABIC_ABJAD_DASH, "a:p/a:pPr/a:buAutoNum{type=arabicAbjadDash}"), + ("a:p/a:pPr/a:buAutoNum", MSO_NUMBERED_BULLET_STYLE.BULLET_TRAD_CHIN_PLAIN, "a:p/a:pPr/a:buAutoNum{type=tradChinPlain}"), ] ) def bullet_set_fixture(self, request): From 241c874cecfd39a81234c888296537709944f741 Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Tue, 29 Apr 2025 16:26:18 +0200 Subject: [PATCH 05/19] Implement numbered bullets (get/set) --- src/pptx/oxml/__init__.py | 2 ++ src/pptx/oxml/text.py | 31 +++++++++++++++++++++++++++---- src/pptx/text/text.py | 6 +++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/pptx/oxml/__init__.py b/src/pptx/oxml/__init__.py index eea1684fd..c2cf3b151 100644 --- a/src/pptx/oxml/__init__.py +++ b/src/pptx/oxml/__init__.py @@ -445,6 +445,7 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]): from pptx.oxml.text import ( # noqa: E402 CT_RegularTextRun, + CT_TextAutoNumberBullet, CT_TextBody, CT_TextBodyProperties, CT_TextCharacterProperties, @@ -479,6 +480,7 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]): register_element_cls("a:spcPct", CT_TextSpacingPercent) register_element_cls("a:spcPts", CT_TextSpacingPoint) register_element_cls('a:buNone', CT_TextNoBullet) +register_element_cls('a:buAutoNum', CT_TextAutoNumberBullet) register_element_cls('a:buChar', CT_TextCharBullet) register_element_cls("a:txBody", CT_TextBody) register_element_cls("c:txPr", CT_TextBody) diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index 9447e350f..344fca036 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -8,6 +8,7 @@ from pptx.enum.lang import MSO_LANGUAGE_ID from pptx.enum.text import ( MSO_AUTO_SIZE, + MSO_NUMBERED_BULLET_STYLE, MSO_TEXT_UNDERLINE_TYPE, MSO_VERTICAL_ANCHOR, PP_PARAGRAPH_ALIGNMENT, @@ -469,11 +470,13 @@ class CT_TextParagraphProperties(BaseOxmlElement): _add_spcBef: Callable[[], CT_TextSpacing] _add_buNone: Callable[[], CT_TextNoBullet] _add_buChar: Callable[[], CT_TextCharBullet] + _add_buAutoNum: Callable[[], CT_TextAutoNumberBullet] _remove_lnSpc: Callable[[], None] _remove_spcAft: Callable[[], None] _remove_spcBef: Callable[[], None] _remove_buNone: Callable[[], None] _remove_buChar: Callable[[], None] + _remove_buAutoNum: Callable[[], CT_TextAutoNumberBullet] _tag_seq = ( "a:lnSpc", @@ -509,6 +512,9 @@ class CT_TextParagraphProperties(BaseOxmlElement): buNone: CT_TextNoBullet | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "a:buNone", successors=_tag_seq[11:] ) + buAutoNum: CT_TextAutoNumberBullet | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] + "a:buAutoNum", successors=_tag_seq[12:] + ) buChar: CT_TextCharBullet | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "a:buChar", successors=_tag_seq[13:] ) @@ -546,18 +552,22 @@ def line_spacing(self, value: float | Length | None): self._add_lnSpc().set_spcPct(value) @property - def bullet(self) -> bool | str | None: + def bullet(self) -> bool | str | MSO_NUMBERED_BULLET_STYLE | None: buNone = self.buNone buChar = self.buChar - if buNone is not None: + buAutoNum = self.buAutoNum + if buNone is not None and buAutoNum is None: return False if buChar is not None: return buChar.char + if buAutoNum is not None: + return buAutoNum.val @bullet.setter - def bullet(self, value: bool | str | None): + def bullet(self, value: bool | str | MSO_NUMBERED_BULLET_STYLE | None): self._remove_buNone() self._remove_buChar() + self._remove_buAutoNum() if value is None: return if isinstance(value, bool): @@ -566,7 +576,9 @@ def bullet(self, value: bool | str | None): buChar.char = u"\u2022" else: self._add_buNone() - + elif isinstance(value, MSO_NUMBERED_BULLET_STYLE): + buAutoNum = self._add_buAutoNum() + buAutoNum.val = value else: buChar = self._add_buChar() buChar.char = value @@ -670,4 +682,15 @@ class CT_TextCharBullet(BaseOxmlElement): char: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] "char", XsdString + ) + + +class CT_TextAutoNumberBullet(BaseOxmlElement): + """ + element, specifying that a paragraph should have an automatically + incremented bullet of the specified `type`. + """ + + val: MSO_NUMBERED_BULLET_STYLE = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "type", MSO_NUMBERED_BULLET_STYLE ) \ No newline at end of file diff --git a/src/pptx/text/text.py b/src/pptx/text/text.py index 02c44cdc8..b9b8328ed 100644 --- a/src/pptx/text/text.py +++ b/src/pptx/text/text.py @@ -7,7 +7,7 @@ from pptx.dml.fill import FillFormat from pptx.enum.dml import MSO_FILL from pptx.enum.lang import MSO_LANGUAGE_ID -from pptx.enum.text import MSO_AUTO_SIZE, MSO_UNDERLINE, MSO_VERTICAL_ANCHOR +from pptx.enum.text import MSO_AUTO_SIZE, MSO_NUMBERED_BULLET_STYLE, MSO_UNDERLINE, MSO_VERTICAL_ANCHOR from pptx.opc.constants import RELATIONSHIP_TYPE as RT from pptx.oxml.simpletypes import ST_TextWrappingType from pptx.shapes import Subshape @@ -548,14 +548,14 @@ def line_spacing(self, value: int | float | Length | None): pPr.line_spacing = value @property - def bullet(self): + def bullet(self) -> bool | str | MSO_NUMBERED_BULLET_STYLE | None: pPr = self._p.pPr if pPr is None: return None return pPr.bullet @bullet.setter - def bullet(self, value: bool | str | None): + def bullet(self, value: bool | str | MSO_NUMBERED_BULLET_STYLE | None): pPr = self._p.get_or_add_pPr() pPr.bullet = value From a97aa5f05be149eefba9321c0b454b5242b4184a Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Tue, 29 Apr 2025 16:54:10 +0200 Subject: [PATCH 06/19] Implement behave test for autonumber bullets --- features/steps/text.py | 9 +++++++-- features/txt-paragraph.feature | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/features/steps/text.py b/features/steps/text.py index 1096f842e..da832e044 100644 --- a/features/steps/text.py +++ b/features/steps/text.py @@ -6,7 +6,7 @@ from helpers import test_pptx from pptx import Presentation -from pptx.enum.text import PP_ALIGN +from pptx.enum.text import MSO_NUMBERED_BULLET_STYLE, PP_ALIGN from pptx.util import Emu # given =================================================== @@ -145,6 +145,7 @@ def when_I_assign_value_to_paragraph_bullet(context, value_str): "True": True, "False": False, "None": None, + "hindiAlphaPeriod": MSO_NUMBERED_BULLET_STYLE.BULLET_HINDI_ALPHA_PERIOD, }[value_str] paragraph = context.paragraph paragraph.bullet = value @@ -259,8 +260,12 @@ def then_run_text_is_not_a_hyperlink(context): def then_font_name_matches_typeface_I_set(context): assert context.font.name == "Verdana" - @then("paragraph.bullet == {value}") def then_paragraph_bullet_is_value(context, value): actual, expected = context.paragraph.bullet, eval(value) assert actual == expected, 'paragraph.bullet == "%s"' % actual + +@then("paragraph.bullet is the right style") +def then_paragraph_bullet_is_value(context): + actual = context.paragraph.bullet + assert actual == MSO_NUMBERED_BULLET_STYLE.BULLET_HINDI_ALPHA_PERIOD, 'paragraph.bullet == "%s"' % actual diff --git a/features/txt-paragraph.feature b/features/txt-paragraph.feature index cc969e36a..383953d75 100644 --- a/features/txt-paragraph.feature +++ b/features/txt-paragraph.feature @@ -101,8 +101,16 @@ Feature: Change paragraph properties Then paragraph.bullet == Examples: _Paragraph assigned bullet replacement cases - | value | expected-value | - | x | "x" | - | False | False | - | True | "\u2022" | - | None | None | + | value | expected-value | + | x | "x" | + | False | False | + | True | "\u2022" | + | None | None | + + Scenario Outline: _Paragraph.bullet setter with style + Given a _Paragraph object as paragraph + When I assign paragraph.bullet = + Then paragraph.bullet is the right style + + Examples: _Paragraph assigned bullet replacement cases + | hindiAlphaPeriod | From afbf3c562620054a1d18fcf48fd13bf671de8ffd Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Tue, 29 Apr 2025 17:06:32 +0200 Subject: [PATCH 07/19] Shorten bullet style names --- features/steps/text.py | 4 +- src/pptx/enum/text.py | 84 ++++++++++++++++++++--------------------- tests/text/test_text.py | 8 ++-- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/features/steps/text.py b/features/steps/text.py index da832e044..d3c1d1cc5 100644 --- a/features/steps/text.py +++ b/features/steps/text.py @@ -145,7 +145,7 @@ def when_I_assign_value_to_paragraph_bullet(context, value_str): "True": True, "False": False, "None": None, - "hindiAlphaPeriod": MSO_NUMBERED_BULLET_STYLE.BULLET_HINDI_ALPHA_PERIOD, + "hindiAlphaPeriod": MSO_NUMBERED_BULLET_STYLE.HINDI_ALPHA_PERIOD, }[value_str] paragraph = context.paragraph paragraph.bullet = value @@ -268,4 +268,4 @@ def then_paragraph_bullet_is_value(context, value): @then("paragraph.bullet is the right style") def then_paragraph_bullet_is_value(context): actual = context.paragraph.bullet - assert actual == MSO_NUMBERED_BULLET_STYLE.BULLET_HINDI_ALPHA_PERIOD, 'paragraph.bullet == "%s"' % actual + assert actual == MSO_NUMBERED_BULLET_STYLE.HINDI_ALPHA_PERIOD, 'paragraph.bullet == "%s"' % actual diff --git a/src/pptx/enum/text.py b/src/pptx/enum/text.py index a13ff2986..2df212807 100644 --- a/src/pptx/enum/text.py +++ b/src/pptx/enum/text.py @@ -236,45 +236,45 @@ class MSO_NUMBERED_BULLET_STYLE(BaseXmlEnum): https://learn.microsoft.com/sv-se/office/vba/api/office.msonumberedbulletstyle """ - BULLET_ALPHA_LC_PAREN_BOTH = (8, "alphaLCParenBoth", "Lowercase alphabetical bullet with opening and closing parentheses.") - BULLET_ALPHA_LC_PAREN_RIGHT = (9, "alphaLCParenRight", "Lowercase alphabetical bullet with closing parenthesis.") - BULLET_ALPHA_LC_PERIOD = (0, "alphaLCPeriod", "Lowercase alphabetical bullet with period.|") - BULLET_ALPHA_UC_PAREN_BOTH = (10, "alphaUCParenBoth", "Uppercase alphabetical bullet with opening and closing parentheses.") - BULLET_ALPHA_UC_PAREN_RIGHT = (11, "alphaUCParenRight", "Uppercase alphabetical bullet with closing parenthesis.") - BULLET_ALPHA_UC_PERIOD = (1, "alphaUCPeriod", "Uppercase alphabetical bullet with period.") - BULLET_ARABIC_ABJAD_DASH = (24, "arabicAbjadDash", "Arabic Abjad bullet with a dash.") - BULLET_ARABIC_ALPHA_DASH = (23, "arabicAlphaDash", "Arabic alphabetical bullet with a dash.") - BULLET_ARABIC_DB_PERIOD = (29, "arabicDBPeriod", "Arabic DB bullet with period.") - BULLET_ARABIC_DB_PLAIN = (28, "arabicDBPlain", "Plain Arabic DB bullet.") - BULLET_ARABIC_PAREN_BOTH = (12, "arabicParenBoth", "Arabic bullet with opening and closing parentheses.") - BULLET_ARABIC_PAREN_RIGHT = (2, "arabicParenRight", "Arabic bullet with closing parenthesis.") - BULLET_ARABIC_PERIOD = (3, "arabicPeriod", "Arabic bullet with period.") - BULLET_ARABIC_PLAIN = (13, "arabicPlain", "Plain Arabic bullet.") - BULLET_CIRCLE_NUM_DB_PLAIN = (18, "circleNumDBPlain", "Circled number bullet.") - BULLET_CIRCLE_NUM_WD_BLACK_PLAIN = (20, "circleNumWDBlackPlain", "Circled number WD black bullet.") - BULLET_CIRCLE_NUM_WD_WHITE_PLAIN = (19, "circleNumWDWhitePlain", "Circled number WD white bullet.") - BULLET_HEBREW_ALPHA_DASH = (25, "hebrewAlphaDash", "Hebrew alphabetical bullet with dash.") - BULLET_HINDI_ALPHA_1PERIOD = (40, "hindiAlpha1Period", "Hindi alphabetical bullet 1 with period.") - BULLET_HINDI_ALPHA_PERIOD = (36, "hindiAlphaPeriod", "Hindi alphabetical bullet with period.") - BULLET_HINDI_NUM_PAREN_RIGHT = (39, "hindiNumParenRight", "Hindi numbered bullet with closing parenthesis.") - BULLET_HINDI_NUM_PERIOD = (37, "hindiNumPeriod", "Hindi numbered bullet with period.") - BULLET_KANJI_KOREAN_PERIOD = (27, "kanjiKoreanPeriod", "Korean Kanji bullet with period.") - BULLET_KANJI_KOREAN_PLAIN = (26, "kanjiKoreanPlain", "Korean Kanji bullet.") - BULLET_KANJI_SIMP_CHIN_DB_PERIOD = (38, "kanjiSimpChinDBPeriod", "Simplified Chinese Kanji bulllet with period.") - BULLET_ROMAN_LC_PAREN_BOTH = (4, "romanLCParenBoth", "Lowercase roman bullet with opening and closing parentheses.") - BULLET_ROMAN_LC_PAREN_RIGHT = (5, "romanLCParenRight", "Lowercase roman bullet with closing parenthesis.") - BULLET_ROMAN_LC_PERIOD = (6, "romanLCPeriod", "Lowercase roman bullet with period.") - BULLET_ROMAN_UC_PAREN_BOTH = (14, "romanUCParenBoth", "Uppercase roman bullet with opening and closing parentheses.") - BULLET_ROMAN_UC_PAREN_RIGHT = (15, "romanUCParenRight", "Uppercase roman bullet with closing parenthesis.") - BULLET_ROMAN_UC_PERIOD = (7, "romanUCPeriod", "Uppercase roman bullet with period.") - BULLET_SIMP_CHIN_PERIOD = (17, "simpChinPeriod", "Simplified Chinese bulllet with period.") - BULLET_SIMP_CHIN_PLAIN = (16, "simpChinPlain", "Simplified Chinese bullet.") - BULLET_STYLE_MIXED = (-2, "styleMixed", "Return value only; indicates a combination of the other states. ") - BULLET_THAI_ALPHA_PAREN_BOTH = (32, "thaiAlphaParenBoth", "Thai alphabetical bullet with opening and closing parentheses.") - BULLET_THAI_ALPHA_PAREN_RIGHT = (31, "thaiAlphaParenRight", "Thai alphabetical bullet with closing parenthesis.") - BULLET_THAI_ALPHA_PERIOD = (30, "thaiAlphaPeriod", "Thai alphabetical bullet with period.") - BULLET_THAI_NUM_PAREN_BOTH = (35, "thaiNumParenBoth", "Thai numerical bullet with opening and closing parentheses.") - BULLET_THAI_NUM_PAREN_RIGHT = (34, "thaiNumParenRight", "Thai numerical bullet with closing parenthesis.") - BULLET_THAI_NUM_PERIOD = (33, "thaiNumPeriod", "Thai numerical bullet with period.") - BULLET_TRAD_CHIN_PERIOD = (22, "tradChinPeriod", "Traditional Chinese bulllet with period.") - BULLET_TRAD_CHIN_PLAIN = (21, "tradChinPlain", "Traditional Chinese bulllet.") \ No newline at end of file + ALPHA_LC_PAREN_BOTH = (8, "alphaLCParenBoth", "Lowercase alphabetical bullet with opening and closing parentheses.") + ALPHA_LC_PAREN_RIGHT = (9, "alphaLCParenRight", "Lowercase alphabetical bullet with closing parenthesis.") + ALPHA_LC_PERIOD = (0, "alphaLCPeriod", "Lowercase alphabetical bullet with period.|") + ALPHA_UC_PAREN_BOTH = (10, "alphaUCParenBoth", "Uppercase alphabetical bullet with opening and closing parentheses.") + ALPHA_UC_PAREN_RIGHT = (11, "alphaUCParenRight", "Uppercase alphabetical bullet with closing parenthesis.") + ALPHA_UC_PERIOD = (1, "alphaUCPeriod", "Uppercase alphabetical bullet with period.") + ARABIC_ABJAD_DASH = (24, "arabicAbjadDash", "Arabic Abjad bullet with a dash.") + ARABIC_ALPHA_DASH = (23, "arabicAlphaDash", "Arabic alphabetical bullet with a dash.") + ARABIC_DB_PERIOD = (29, "arabicDBPeriod", "Arabic DB bullet with period.") + ARABIC_DB_PLAIN = (28, "arabicDBPlain", "Plain Arabic DB bullet.") + ARABIC_PAREN_BOTH = (12, "arabicParenBoth", "Arabic bullet with opening and closing parentheses.") + ARABIC_PAREN_RIGHT = (2, "arabicParenRight", "Arabic bullet with closing parenthesis.") + ARABIC_PERIOD = (3, "arabicPeriod", "Arabic bullet with period.") + ARABIC_PLAIN = (13, "arabicPlain", "Plain Arabic bullet.") + CIRCLE_NUM_DB_PLAIN = (18, "circleNumDBPlain", "Circled number bullet.") + CIRCLE_NUM_WD_BLACK_PLAIN = (20, "circleNumWDBlackPlain", "Circled number WD black bullet.") + CIRCLE_NUM_WD_WHITE_PLAIN = (19, "circleNumWDWhitePlain", "Circled number WD white bullet.") + HEBREW_ALPHA_DASH = (25, "hebrewAlphaDash", "Hebrew alphabetical bullet with dash.") + HINDI_ALPHA1_PERIOD = (40, "hindiAlpha1Period", "Hindi alphabetical bullet 1 with period.") + HINDI_ALPHA_PERIOD = (36, "hindiAlphaPeriod", "Hindi alphabetical bullet with period.") + HINDI_NUM_PAREN_RIGHT = (39, "hindiNumParenRight", "Hindi numbered bullet with closing parenthesis.") + HINDI_NUM_PERIOD = (37, "hindiNumPeriod", "Hindi numbered bullet with period.") + KANJI_KOREAN_PERIOD = (27, "kanjiKoreanPeriod", "Korean Kanji bullet with period.") + KANJI_KOREAN_PLAIN = (26, "kanjiKoreanPlain", "Korean Kanji bullet.") + KANJI_SIMP_CHIN_DB_PERIOD = (38, "kanjiSimpChinDBPeriod", "Simplified Chinese Kanji bulllet with period.") + ROMAN_LC_PAREN_BOTH = (4, "romanLCParenBoth", "Lowercase roman bullet with opening and closing parentheses.") + ROMAN_LC_PAREN_RIGHT = (5, "romanLCParenRight", "Lowercase roman bullet with closing parenthesis.") + ROMAN_LC_PERIOD = (6, "romanLCPeriod", "Lowercase roman bullet with period.") + ROMAN_UC_PAREN_BOTH = (14, "romanUCParenBoth", "Uppercase roman bullet with opening and closing parentheses.") + ROMAN_UC_PAREN_RIGHT = (15, "romanUCParenRight", "Uppercase roman bullet with closing parenthesis.") + ROMAN_UC_PERIOD = (7, "romanUCPeriod", "Uppercase roman bullet with period.") + SIMP_CHIN_PERIOD = (17, "simpChinPeriod", "Simplified Chinese bulllet with period.") + SIMP_CHIN_PLAIN = (16, "simpChinPlain", "Simplified Chinese bullet.") + STYLE_MIXED = (-2, "styleMixed", "Return value only; indicates a combination of the other states. ") + THAI_ALPHA_PAREN_BOTH = (32, "thaiAlphaParenBoth", "Thai alphabetical bullet with opening and closing parentheses.") + THAI_ALPHA_PAREN_RIGHT = (31, "thaiAlphaParenRight", "Thai alphabetical bullet with closing parenthesis.") + THAI_ALPHA_PERIOD = (30, "thaiAlphaPeriod", "Thai alphabetical bullet with period.") + THAI_NUM_PAREN_BOTH = (35, "thaiNumParenBoth", "Thai numerical bullet with opening and closing parentheses.") + THAI_NUM_PAREN_RIGHT = (34, "thaiNumParenRight", "Thai numerical bullet with closing parenthesis.") + THAI_NUM_PERIOD = (33, "thaiNumPeriod", "Thai numerical bullet with period.") + TRAD_CHIN_PERIOD = (22, "tradChinPeriod", "Traditional Chinese bulllet with period.") + TRAD_CHIN_PLAIN = (21, "tradChinPlain", "Traditional Chinese bulllet.") \ No newline at end of file diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 9288d8ef6..f13f609a8 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -1143,10 +1143,10 @@ def text_get_fixture(self, request): ("a:p/a:pPr/a:buNone", None, "a:p/a:pPr"), ("a:p/a:pPr/a:buChar", None, "a:p/a:pPr"), ("a:p/a:pPr/a:buAutoNum", None, "a:p/a:pPr"), - ("a:p", MSO_NUMBERED_BULLET_STYLE.BULLET_ROMAN_UC_PERIOD, "a:p/a:pPr/a:buAutoNum{type=romanUCPeriod}"), - ("a:p/a:pPr/a:buChar", MSO_NUMBERED_BULLET_STYLE.BULLET_ALPHA_LC_PERIOD, "a:p/a:pPr/a:buAutoNum{type=alphaLCPeriod}"), - ("a:p/a:pPr/a:buNone", MSO_NUMBERED_BULLET_STYLE.BULLET_ARABIC_ABJAD_DASH, "a:p/a:pPr/a:buAutoNum{type=arabicAbjadDash}"), - ("a:p/a:pPr/a:buAutoNum", MSO_NUMBERED_BULLET_STYLE.BULLET_TRAD_CHIN_PLAIN, "a:p/a:pPr/a:buAutoNum{type=tradChinPlain}"), + ("a:p", MSO_NUMBERED_BULLET_STYLE.ROMAN_UC_PERIOD, "a:p/a:pPr/a:buAutoNum{type=romanUCPeriod}"), + ("a:p/a:pPr/a:buChar", MSO_NUMBERED_BULLET_STYLE.ALPHA_LC_PERIOD, "a:p/a:pPr/a:buAutoNum{type=alphaLCPeriod}"), + ("a:p/a:pPr/a:buNone", MSO_NUMBERED_BULLET_STYLE.ARABIC_ABJAD_DASH, "a:p/a:pPr/a:buAutoNum{type=arabicAbjadDash}"), + ("a:p/a:pPr/a:buAutoNum", MSO_NUMBERED_BULLET_STYLE.TRAD_CHIN_PLAIN, "a:p/a:pPr/a:buAutoNum{type=tradChinPlain}"), ] ) def bullet_set_fixture(self, request): From 04128790e6c993157cb3eb5b8a9d32f2cea60f1a Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Tue, 29 Apr 2025 17:30:23 +0200 Subject: [PATCH 08/19] Add comments to MSO_NUMBERED_BULLET_STYLE --- src/pptx/enum/text.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pptx/enum/text.py b/src/pptx/enum/text.py index 2df212807..5a4c0e631 100644 --- a/src/pptx/enum/text.py +++ b/src/pptx/enum/text.py @@ -231,8 +231,16 @@ class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum): class MSO_NUMBERED_BULLET_STYLE(BaseXmlEnum): - """ + """Specifies the style of numbered bullets. + + Example:: + + from pptx.enum.text import MSO_NUMBERED_BULLET_STYLE + + shape.paragraphs[0].bullet = MSO_NUMBERED_BULLET_STYLE.BULLET_ALPHA_UC_PERIOD + MS API Name: `MsoNumberedBulletStyle` + https://learn.microsoft.com/sv-se/office/vba/api/office.msonumberedbulletstyle """ From 6bfc24598b361a25210804ea6e42f46e3b2285b6 Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Tue, 29 Apr 2025 17:30:51 +0200 Subject: [PATCH 09/19] Add comments to bullet getter in oxml.text --- src/pptx/oxml/text.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index 344fca036..93b050ded 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -553,15 +553,23 @@ def line_spacing(self, value: float | Length | None): @property def bullet(self) -> bool | str | MSO_NUMBERED_BULLET_STYLE | None: + """The type of bullet used for this paragraph. + + A string value means that the paragraph has a bullet set to this string. If the value + is an |MSO_NUMBERED_BULLET_STYLE|, then the paragraph's bullet is automatically + numbered according to the corresponding style. |False| indicates that bullets are + turned off for this paragraph. |None| indicates that a bullet are not displayed for + this paragraph but that it is not actively turned off. + """ buNone = self.buNone buChar = self.buChar buAutoNum = self.buAutoNum - if buNone is not None and buAutoNum is None: - return False if buChar is not None: return buChar.char if buAutoNum is not None: return buAutoNum.val + if buNone is not None: + return False @bullet.setter def bullet(self, value: bool | str | MSO_NUMBERED_BULLET_STYLE | None): From 75090be141c9e76f9af23d1cb02ad483b46153f5 Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Tue, 29 Apr 2025 19:23:44 +0200 Subject: [PATCH 10/19] Update docs for bullet getters/setters --- src/pptx/oxml/text.py | 4 ++-- src/pptx/text/text.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index 93b050ded..8595620af 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -558,8 +558,8 @@ def bullet(self) -> bool | str | MSO_NUMBERED_BULLET_STYLE | None: A string value means that the paragraph has a bullet set to this string. If the value is an |MSO_NUMBERED_BULLET_STYLE|, then the paragraph's bullet is automatically numbered according to the corresponding style. |False| indicates that bullets are - turned off for this paragraph. |None| indicates that a bullet are not displayed for - this paragraph but that it is not actively turned off. + turned off for this paragraph. |None| indicates that a bullet exists if the template + of the current slide defines this as the default form for paragraphs. """ buNone = self.buNone buChar = self.buChar diff --git a/src/pptx/text/text.py b/src/pptx/text/text.py index b9b8328ed..af4f2b101 100644 --- a/src/pptx/text/text.py +++ b/src/pptx/text/text.py @@ -549,6 +549,14 @@ def line_spacing(self, value: int | float | Length | None): @property def bullet(self) -> bool | str | MSO_NUMBERED_BULLET_STYLE | None: + """The type of bullet used for this paragraph. + + A string value means that the paragraph has a bullet set to this string. If the value + is an |MSO_NUMBERED_BULLET_STYLE|, then the paragraph's bullet is automatically + numbered according to the corresponding style. |False| indicates that bullets are + turned off for this paragraph. |None| indicates that a bullet exists if the template + of the current slide defines this as the default form for paragraphs. + """ pPr = self._p.pPr if pPr is None: return None From b6f7ffc869f1094fbc44bc7e16aca85fe932794e Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Wed, 30 Apr 2025 10:58:06 +0200 Subject: [PATCH 11/19] Update documentation for `p.bullet = None` --- docs/user/text.rst | 26 ++++++++++++++++++++++++++ src/pptx/oxml/text.py | 4 ++-- src/pptx/text/text.py | 4 ++-- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/user/text.rst b/docs/user/text.rst index 1f59c8fa3..935b866d4 100644 --- a/docs/user/text.rst +++ b/docs/user/text.rst @@ -142,6 +142,32 @@ second and third indented (like sub-bullets) under the first:: p.level = 1 +Applying paragraph bullet formatting +----------------------------- + +The following continues from the previous example, changes the first bullet +to an "x", and changes the second and third sub-bullets to be numbered:: + + from pptx.enum.text import MSO_NUMBERED_BULLET_STYLE + + p = text_frame.paragraphs[0] + p.bullet = "x" + + for p in text_frame.paragraphs[1:]: + p = MSO_NUMBERED_BULLET_STYLE.ARABIC_PERIOD + +The ``.bullet`` attribute can also be used to remove bullet formatting:: + + p = text_frame.add_paragraph() + p.text = "This is not a bullet!" + p.bullet = False + +Finally, the attribute can also be used to revert a paragraph back to the +slide's default bullet configuration:: + + p.text = "Now it's a bullet again." + p.bullet = None + Applying character formatting ----------------------------- diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index 8595620af..3f9b69926 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -558,8 +558,8 @@ def bullet(self) -> bool | str | MSO_NUMBERED_BULLET_STYLE | None: A string value means that the paragraph has a bullet set to this string. If the value is an |MSO_NUMBERED_BULLET_STYLE|, then the paragraph's bullet is automatically numbered according to the corresponding style. |False| indicates that bullets are - turned off for this paragraph. |None| indicates that a bullet exists if the template - of the current slide defines this as the default form for paragraphs. + turned off for this paragraph. |None| indicates that a bullet exists if the master or + slide layout defines this as the default for paragraphs. """ buNone = self.buNone buChar = self.buChar diff --git a/src/pptx/text/text.py b/src/pptx/text/text.py index af4f2b101..e8214a2fb 100644 --- a/src/pptx/text/text.py +++ b/src/pptx/text/text.py @@ -554,8 +554,8 @@ def bullet(self) -> bool | str | MSO_NUMBERED_BULLET_STYLE | None: A string value means that the paragraph has a bullet set to this string. If the value is an |MSO_NUMBERED_BULLET_STYLE|, then the paragraph's bullet is automatically numbered according to the corresponding style. |False| indicates that bullets are - turned off for this paragraph. |None| indicates that a bullet exists if the template - of the current slide defines this as the default form for paragraphs. + turned off for this paragraph. |None| indicates that a bullet exists if the master or + slide layout defines this as the default for paragraphs. """ pPr = self._p.pPr if pPr is None: From b3b98aef3cc245265991e5d69678750fb22068df Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Wed, 30 Apr 2025 11:01:11 +0200 Subject: [PATCH 12/19] Add back trailing newline --- src/pptx/enum/text.py | 2 +- src/pptx/oxml/text.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pptx/enum/text.py b/src/pptx/enum/text.py index 5a4c0e631..5dd0d546d 100644 --- a/src/pptx/enum/text.py +++ b/src/pptx/enum/text.py @@ -285,4 +285,4 @@ class MSO_NUMBERED_BULLET_STYLE(BaseXmlEnum): THAI_NUM_PAREN_RIGHT = (34, "thaiNumParenRight", "Thai numerical bullet with closing parenthesis.") THAI_NUM_PERIOD = (33, "thaiNumPeriod", "Thai numerical bullet with period.") TRAD_CHIN_PERIOD = (22, "tradChinPeriod", "Traditional Chinese bulllet with period.") - TRAD_CHIN_PLAIN = (21, "tradChinPlain", "Traditional Chinese bulllet.") \ No newline at end of file + TRAD_CHIN_PLAIN = (21, "tradChinPlain", "Traditional Chinese bulllet.") diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index 3f9b69926..e41492368 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -701,4 +701,4 @@ class CT_TextAutoNumberBullet(BaseOxmlElement): val: MSO_NUMBERED_BULLET_STYLE = RequiredAttribute( # pyright: ignore[reportAssignmentType] "type", MSO_NUMBERED_BULLET_STYLE - ) \ No newline at end of file + ) From b95cab01e1cc54d7664863af47e250030739686c Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Wed, 30 Apr 2025 11:06:51 +0200 Subject: [PATCH 13/19] Make order of tag-related methods consistent --- src/pptx/oxml/text.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index e41492368..f656da19b 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -469,14 +469,14 @@ class CT_TextParagraphProperties(BaseOxmlElement): _add_spcAft: Callable[[], CT_TextSpacing] _add_spcBef: Callable[[], CT_TextSpacing] _add_buNone: Callable[[], CT_TextNoBullet] - _add_buChar: Callable[[], CT_TextCharBullet] _add_buAutoNum: Callable[[], CT_TextAutoNumberBullet] + _add_buChar: Callable[[], CT_TextCharBullet] _remove_lnSpc: Callable[[], None] _remove_spcAft: Callable[[], None] _remove_spcBef: Callable[[], None] _remove_buNone: Callable[[], None] - _remove_buChar: Callable[[], None] _remove_buAutoNum: Callable[[], CT_TextAutoNumberBullet] + _remove_buChar: Callable[[], None] _tag_seq = ( "a:lnSpc", From f88aecb55ac89450deb95c9f78b79ac325e60ade Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Wed, 30 Apr 2025 15:56:19 +0200 Subject: [PATCH 14/19] Define ``BulletStyle`` for controlling bullet style Using ``False`` (but not ``True``), ``None``, and ``MSO_NUMBERED_BULLET_STYLE`` felt a bit messy. ``True`` (as in, use a default bullet) would be nice, but there is currently no way to infer what bullet to use. From my understanding, determining the bullet would requireus to parse the slide master and layout. Setting a "global" default could cause unexpected behaviour, and it is better that users explicitly choose a bullet. --- src/pptx/enum/text.py | 9 ++++++++ src/pptx/util.py | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/pptx/enum/text.py b/src/pptx/enum/text.py index 5dd0d546d..66df44658 100644 --- a/src/pptx/enum/text.py +++ b/src/pptx/enum/text.py @@ -1,6 +1,7 @@ """Enumerations used by text and related objects.""" from __future__ import annotations +from enum import Enum, auto from pptx.enum.base import BaseEnum, BaseXmlEnum @@ -230,6 +231,14 @@ class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum): PP_ALIGN = PP_PARAGRAPH_ALIGNMENT +class BulletStyleType(Enum): + """Types of bullet styles.""" + NO_BULLET = auto() + CUSTOM = auto() + NUMBERED = auto() + DEFAULT = auto() + + class MSO_NUMBERED_BULLET_STYLE(BaseXmlEnum): """Specifies the style of numbered bullets. diff --git a/src/pptx/util.py b/src/pptx/util.py index fdec79298..8887fcb20 100644 --- a/src/pptx/util.py +++ b/src/pptx/util.py @@ -5,6 +5,8 @@ import functools from typing import Any, Callable, Generic, TypeVar, cast +from pptx.enum.text import MSO_NUMBERED_BULLET_STYLE, BulletStyleType + class Length(int): """Base class for length classes Inches, Emu, Cm, Mm, and Pt. @@ -102,6 +104,57 @@ def __new__(cls, points: float): return Length.__new__(cls, emu) +class BulletStyle: + """Convenience value class for styling unnumbered bullets. + + ``BulletStyle.NO_BULLET`` indicates that bullets are explicitly disabled + a paragraph. ``BulletStyle.DEFAULT`` indicates that whether the paragraph + is rendered as a bullet is defined in the slide master or layout. + + The methods ``BulletStyle.custom`` and ``BulletStyle.numbered`` can be + used to create ``BulletStyle``s that control what kind of bullet is used + for the paragraph. + """ + + NO_BULLET: BulletStyle = None + DEFAULT: BulletStyle = None + + def __init__(self, style: BulletStyleType, value: str | MSO_NUMBERED_BULLET_STYLE | None = None): + self._style = style + self._value = value + + def __eq__(self, other: object): + if isinstance(other, BulletStyle) and self._style == other._style: + return self._value == other._value + else: + return False + + @property + def value(self) -> str | MSO_NUMBERED_BULLET_STYLE | None: + return self._value + + @property + def style(self) -> BulletStyleType: + return self._style + + @classmethod + def custom(cls, bullet_string: str): + """Defines a bullet that is rendered as ``bullet_string``.""" + return BulletStyle(BulletStyleType.CUSTOM, bullet_string) + + @classmethod + def numbered(cls, style: MSO_NUMBERED_BULLET_STYLE): + """Defines a bullet that is numbered. + + The style of the enumeration is controlled by the ``style`` and + is a ``MSO_NUMBERED_BULLET_STYLE``. + """ + return BulletStyle(BulletStyleType.NUMBERED, style) + +BulletStyle.NO_BULLET = BulletStyle(BulletStyleType.NO_BULLET) +BulletStyle.DEFAULT = BulletStyle(BulletStyleType.DEFAULT) + + _T = TypeVar("_T") From f1c9176e2964a177d836664842b6d388961821df Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Wed, 30 Apr 2025 15:56:36 +0200 Subject: [PATCH 15/19] Add tests for BulletStyle logic --- tests/text/test_text.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/tests/text/test_text.py b/tests/text/test_text.py index f13f609a8..0a4c634b3 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -16,7 +16,7 @@ from pptx.opc.package import XmlPart from pptx.shapes.autoshape import Shape from pptx.text.text import Font, TextFrame, _Hyperlink, _Paragraph, _Run -from pptx.util import Inches, Pt +from pptx.util import BulletStyle, Inches, Pt from ..oxml.unitdata.text import a_p, a_t, an_hlinkClick, an_r, an_rPr from ..unitutil.cxml import element, xml @@ -1127,26 +1127,22 @@ def text_get_fixture(self, request): @pytest.fixture( params=[ - ("a:p", "x", "a:p/a:pPr/a:buChar{char=x}"), - ("a:p/a:pPr/a:buChar", "x", "a:p/a:pPr/a:buChar{char=x}"), - ("a:p/a:pPr/a:buNone", "x", "a:p/a:pPr/a:buChar{char=x}"), - ("a:p/a:pPr/a:buAutoNum", "x", "a:p/a:pPr/a:buChar{char=x}"), - ("a:p", True, u"a:p/a:pPr/a:buChar{char=\u2022}"), - ("a:p/a:pPr/a:buChar", True, u"a:p/a:pPr/a:buChar{char=\u2022}"), - ("a:p/a:pPr/a:buNone", True, u"a:p/a:pPr/a:buChar{char=\u2022}"), - ("a:p/a:pPr/a:buAutoNum", True, u"a:p/a:pPr/a:buChar{char=\u2022}"), - ("a:p", False, "a:p/a:pPr/a:buNone"), - ("a:p/a:pPr/a:buChar", False, "a:p/a:pPr/a:buNone"), - ("a:p/a:pPr/a:buNone", False, "a:p/a:pPr/a:buNone"), - ("a:p/a:pPr/a:buAutoNum", False, "a:p/a:pPr/a:buNone"), - ("a:p", None, "a:p/a:pPr"), - ("a:p/a:pPr/a:buNone", None, "a:p/a:pPr"), - ("a:p/a:pPr/a:buChar", None, "a:p/a:pPr"), - ("a:p/a:pPr/a:buAutoNum", None, "a:p/a:pPr"), - ("a:p", MSO_NUMBERED_BULLET_STYLE.ROMAN_UC_PERIOD, "a:p/a:pPr/a:buAutoNum{type=romanUCPeriod}"), - ("a:p/a:pPr/a:buChar", MSO_NUMBERED_BULLET_STYLE.ALPHA_LC_PERIOD, "a:p/a:pPr/a:buAutoNum{type=alphaLCPeriod}"), - ("a:p/a:pPr/a:buNone", MSO_NUMBERED_BULLET_STYLE.ARABIC_ABJAD_DASH, "a:p/a:pPr/a:buAutoNum{type=arabicAbjadDash}"), - ("a:p/a:pPr/a:buAutoNum", MSO_NUMBERED_BULLET_STYLE.TRAD_CHIN_PLAIN, "a:p/a:pPr/a:buAutoNum{type=tradChinPlain}"), + ("a:p", BulletStyle.custom("x"), "a:p/a:pPr/a:buChar{char=x}"), + ("a:p/a:pPr/a:buChar", BulletStyle.custom("x"), "a:p/a:pPr/a:buChar{char=x}"), + ("a:p/a:pPr/a:buNone", BulletStyle.custom("x"), "a:p/a:pPr/a:buChar{char=x}"), + ("a:p/a:pPr/a:buAutoNum", BulletStyle.custom("x"), "a:p/a:pPr/a:buChar{char=x}"), + ("a:p", BulletStyle.NO_BULLET, "a:p/a:pPr/a:buNone"), + ("a:p/a:pPr/a:buChar", BulletStyle.NO_BULLET, "a:p/a:pPr/a:buNone"), + ("a:p/a:pPr/a:buNone", BulletStyle.NO_BULLET, "a:p/a:pPr/a:buNone"), + ("a:p/a:pPr/a:buAutoNum", BulletStyle.NO_BULLET, "a:p/a:pPr/a:buNone"), + ("a:p", BulletStyle.DEFAULT, "a:p/a:pPr"), + ("a:p/a:pPr/a:buNone", BulletStyle.DEFAULT, "a:p/a:pPr"), + ("a:p/a:pPr/a:buChar", BulletStyle.DEFAULT, "a:p/a:pPr"), + ("a:p/a:pPr/a:buAutoNum", BulletStyle.DEFAULT, "a:p/a:pPr"), + ("a:p", BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.ROMAN_UC_PERIOD), "a:p/a:pPr/a:buAutoNum{type=romanUCPeriod}"), + ("a:p/a:pPr/a:buChar", BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.ALPHA_LC_PERIOD), "a:p/a:pPr/a:buAutoNum{type=alphaLCPeriod}"), + ("a:p/a:pPr/a:buNone", BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.ARABIC_ABJAD_DASH), "a:p/a:pPr/a:buAutoNum{type=arabicAbjadDash}"), + ("a:p/a:pPr/a:buAutoNum", BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.TRAD_CHIN_PLAIN), "a:p/a:pPr/a:buAutoNum{type=tradChinPlain}"), ] ) def bullet_set_fixture(self, request): From e30c97d51921b95c0a60238fcbf76559f95ceaae Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Wed, 30 Apr 2025 15:56:55 +0200 Subject: [PATCH 16/19] Implement bullet control based on `BulletStyle` --- src/pptx/oxml/text.py | 50 +++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/pptx/oxml/text.py b/src/pptx/oxml/text.py index f656da19b..c6354f0e3 100644 --- a/src/pptx/oxml/text.py +++ b/src/pptx/oxml/text.py @@ -12,6 +12,7 @@ MSO_TEXT_UNDERLINE_TYPE, MSO_VERTICAL_ANCHOR, PP_PARAGRAPH_ALIGNMENT, + BulletStyleType, ) from pptx.exc import InvalidXmlError from pptx.oxml import parse_xml @@ -40,7 +41,7 @@ ZeroOrOne, ZeroOrOneChoice, ) -from pptx.util import Emu, Length +from pptx.util import BulletStyle, Emu, Length if TYPE_CHECKING: from pptx.oxml.action import CT_Hyperlink @@ -552,44 +553,37 @@ def line_spacing(self, value: float | Length | None): self._add_lnSpc().set_spcPct(value) @property - def bullet(self) -> bool | str | MSO_NUMBERED_BULLET_STYLE | None: - """The type of bullet used for this paragraph. - - A string value means that the paragraph has a bullet set to this string. If the value - is an |MSO_NUMBERED_BULLET_STYLE|, then the paragraph's bullet is automatically - numbered according to the corresponding style. |False| indicates that bullets are - turned off for this paragraph. |None| indicates that a bullet exists if the master or - slide layout defines this as the default for paragraphs. - """ + def bullet(self) -> BulletStyle: + """The style of bullet used for this paragraph.""" buNone = self.buNone buChar = self.buChar buAutoNum = self.buAutoNum + if buChar is not None: - return buChar.char - if buAutoNum is not None: - return buAutoNum.val - if buNone is not None: - return False + return BulletStyle.custom(buChar.char) + elif buAutoNum is not None: + return BulletStyle.numbered(buAutoNum.val) + elif buNone is not None: + return BulletStyle.NO_BULLET + else: + return BulletStyle.DEFAULT @bullet.setter - def bullet(self, value: bool | str | MSO_NUMBERED_BULLET_STYLE | None): + def bullet(self, value: BulletStyle): self._remove_buNone() self._remove_buChar() self._remove_buAutoNum() - if value is None: + + if value == BulletStyle.DEFAULT: return - if isinstance(value, bool): - if value: - buChar = self._add_buChar() - buChar.char = u"\u2022" - else: - self._add_buNone() - elif isinstance(value, MSO_NUMBERED_BULLET_STYLE): - buAutoNum = self._add_buAutoNum() - buAutoNum.val = value - else: + elif value == BulletStyle.NO_BULLET: + self._add_buNone() + elif value.style == BulletStyleType.CUSTOM: buChar = self._add_buChar() - buChar.char = value + buChar.char = cast(str, value.value) + elif value.style == BulletStyleType.NUMBERED: + buAutoNum = self._add_buAutoNum() + buAutoNum.val = cast(MSO_NUMBERED_BULLET_STYLE, value.value) @property From a881f26bad66cd8f0f7752ee69272b081a432c51 Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Wed, 30 Apr 2025 16:21:10 +0200 Subject: [PATCH 17/19] Add behave tests They don't do very much now that the interaction is done by passing `BulletStyle` objects. --- features/steps/text.py | 23 ++++++++++++----------- features/txt-paragraph.feature | 18 +++++------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/features/steps/text.py b/features/steps/text.py index d3c1d1cc5..20422662e 100644 --- a/features/steps/text.py +++ b/features/steps/text.py @@ -7,7 +7,7 @@ from pptx import Presentation from pptx.enum.text import MSO_NUMBERED_BULLET_STYLE, PP_ALIGN -from pptx.util import Emu +from pptx.util import BulletStyle, Emu # given =================================================== @@ -141,11 +141,10 @@ def when_set_hyperlink_address(context): @when("I assign paragraph.bullet = {value_str}") def when_I_assign_value_to_paragraph_bullet(context, value_str): value = { - "x": "x", - "True": True, - "False": False, - "None": None, - "hindiAlphaPeriod": MSO_NUMBERED_BULLET_STYLE.HINDI_ALPHA_PERIOD, + "x": BulletStyle.custom("x"), + "Default": BulletStyle.DEFAULT, + "No bullet": BulletStyle.NO_BULLET, + "hindiAlphaPeriod": BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.HINDI_ALPHA_PERIOD), }[value_str] paragraph = context.paragraph paragraph.bullet = value @@ -262,10 +261,12 @@ def then_font_name_matches_typeface_I_set(context): @then("paragraph.bullet == {value}") def then_paragraph_bullet_is_value(context, value): - actual, expected = context.paragraph.bullet, eval(value) - assert actual == expected, 'paragraph.bullet == "%s"' % actual + expected = { + "x": BulletStyle.custom("x"), + "Default": BulletStyle.DEFAULT, + "No bullet": BulletStyle.NO_BULLET, + "hindiAlphaPeriod": BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.HINDI_ALPHA_PERIOD), + }[value] -@then("paragraph.bullet is the right style") -def then_paragraph_bullet_is_value(context): actual = context.paragraph.bullet - assert actual == MSO_NUMBERED_BULLET_STYLE.HINDI_ALPHA_PERIOD, 'paragraph.bullet == "%s"' % actual + assert actual == expected, 'paragraph.bullet == "%s"' % actual \ No newline at end of file diff --git a/features/txt-paragraph.feature b/features/txt-paragraph.feature index 383953d75..9b6eb79a5 100644 --- a/features/txt-paragraph.feature +++ b/features/txt-paragraph.feature @@ -98,19 +98,11 @@ Feature: Change paragraph properties Scenario Outline: _Paragraph.bullet setter Given a _Paragraph object as paragraph When I assign paragraph.bullet = - Then paragraph.bullet == + Then paragraph.bullet == Examples: _Paragraph assigned bullet replacement cases | value | expected-value | - | x | "x" | - | False | False | - | True | "\u2022" | - | None | None | - - Scenario Outline: _Paragraph.bullet setter with style - Given a _Paragraph object as paragraph - When I assign paragraph.bullet = - Then paragraph.bullet is the right style - - Examples: _Paragraph assigned bullet replacement cases - | hindiAlphaPeriod | + | x | x | + | No bullet | No bullet | + | Default | Default | + | hindiAlphaPeriod | hindiAlphaPeriod | \ No newline at end of file From 90910bfc0cacd9c05b940fc7026a664c5e4b2f1e Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Wed, 30 Apr 2025 16:33:17 +0200 Subject: [PATCH 18/19] Update examples in documentation --- docs/user/text.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/user/text.rst b/docs/user/text.rst index 935b866d4..327f9ddc8 100644 --- a/docs/user/text.rst +++ b/docs/user/text.rst @@ -149,24 +149,25 @@ The following continues from the previous example, changes the first bullet to an "x", and changes the second and third sub-bullets to be numbered:: from pptx.enum.text import MSO_NUMBERED_BULLET_STYLE + from pptx.util import BulletStyle p = text_frame.paragraphs[0] - p.bullet = "x" + p.bullet = BulletStyle.custom("x") for p in text_frame.paragraphs[1:]: - p = MSO_NUMBERED_BULLET_STYLE.ARABIC_PERIOD + p.bullet = BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.ARABIC_PERIOD) The ``.bullet`` attribute can also be used to remove bullet formatting:: p = text_frame.add_paragraph() p.text = "This is not a bullet!" - p.bullet = False + p.bullet = BulletStyle.NO_BULLET Finally, the attribute can also be used to revert a paragraph back to the slide's default bullet configuration:: p.text = "Now it's a bullet again." - p.bullet = None + p.bullet = BulletStyle.DEFAULT Applying character formatting ----------------------------- From 11210aa2742c9166c730fb10173bca7d985ded78 Mon Sep 17 00:00:00 2001 From: Thomas Vakili Date: Sun, 4 May 2025 00:54:10 +0200 Subject: [PATCH 19/19] Update text.py bullet to new interface --- src/pptx/text/text.py | 22 ++++++++++++---------- tests/text/test_text.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/pptx/text/text.py b/src/pptx/text/text.py index e8214a2fb..cd17f73cf 100644 --- a/src/pptx/text/text.py +++ b/src/pptx/text/text.py @@ -13,7 +13,7 @@ from pptx.shapes import Subshape from pptx.text.fonts import FontFiles from pptx.text.layout import TextFitter -from pptx.util import Centipoints, Emu, Length, Pt, lazyproperty +from pptx.util import BulletStyle, Centipoints, Emu, Length, Pt, lazyproperty if TYPE_CHECKING: from pptx.dml.color import ColorFormat @@ -548,22 +548,24 @@ def line_spacing(self, value: int | float | Length | None): pPr.line_spacing = value @property - def bullet(self) -> bool | str | MSO_NUMBERED_BULLET_STYLE | None: - """The type of bullet used for this paragraph. + def bullet(self) -> BulletStyle: + """The type of bullet, if any, defined for this paragraph. - A string value means that the paragraph has a bullet set to this string. If the value - is an |MSO_NUMBERED_BULLET_STYLE|, then the paragraph's bullet is automatically - numbered according to the corresponding style. |False| indicates that bullets are - turned off for this paragraph. |None| indicates that a bullet exists if the master or - slide layout defines this as the default for paragraphs. + ``BulletStyle.NO_BULLET`` indicates that bullets are explicitly disabled + a paragraph. ``BulletStyle.DEFAULT`` indicates that whether the paragraph + is rendered as a bullet is defined in the slide master or layout. + + The methods ``BulletStyle.custom`` and ``BulletStyle.numbered`` can be + used to create ``BulletStyle``s that control what kind of bullet is used + for the paragraph. """ pPr = self._p.pPr if pPr is None: - return None + return BulletStyle.DEFAULT return pPr.bullet @bullet.setter - def bullet(self, value: bool | str | MSO_NUMBERED_BULLET_STYLE | None): + def bullet(self, value: BulletStyle): pPr = self._p.get_or_add_pPr() pPr.bullet = value diff --git a/tests/text/test_text.py b/tests/text/test_text.py index 0a4c634b3..e90dc6205 100644 --- a/tests/text/test_text.py +++ b/tests/text/test_text.py @@ -875,6 +875,11 @@ def it_can_change_its_bullet(self, bullet_set_fixture): paragraph.bullet = new_value assert paragraph._element.xml == expected_xml + def it_knows_its_bullet(self, bullet_get_fixture): + paragraph, expected_bullet = bullet_get_fixture + print(paragraph.bullet, expected_bullet) + assert paragraph.bullet == expected_bullet + @pytest.mark.parametrize( ("p_cxml", "value", "expected_cxml"), [ @@ -1151,6 +1156,19 @@ def bullet_set_fixture(self, request): expected_xml = xml(expected_p_cxml) return paragraph, new_value, expected_xml + @pytest.fixture( + params=[ + ("a:p", BulletStyle.DEFAULT), + ("a:p/a:pPr", BulletStyle.DEFAULT), + ("a:p/a:pPr/a:buNone", BulletStyle.NO_BULLET), + ("a:p/a:pPr/a:buChar{char=x}", BulletStyle.custom("x")), + ("a:p/a:pPr/a:buAutoNum{type=romanUCPeriod}", BulletStyle.numbered(MSO_NUMBERED_BULLET_STYLE.ROMAN_UC_PERIOD)), + ] + ) + def bullet_get_fixture(self, request): + p_cxml, expected_bullet = request.param + paragraph = _Paragraph(element(p_cxml), None) + return paragraph, expected_bullet # fixture components -----------------------------------