diff --git a/docs/api/enum/WdTabAlignment.rst b/docs/api/enum/WdTabAlignment.rst new file mode 100644 index 000000000..a4adb0fc9 --- /dev/null +++ b/docs/api/enum/WdTabAlignment.rst @@ -0,0 +1,38 @@ +.. _WdTabAlignment: + +``WD_TAB_ALIGNMENT`` +==================== + +Specifies the tab stop alignment to apply. + +---- + +LEFT + Left-aligned. + +CENTER + Center-aligned. + +RIGHT + Right-aligned. + +DECIMAL + Decimal-aligned. + +BAR + Bar-aligned. + +LIST + List-aligned. (deprecated) + +CLEAR + Clear an inherited tab stop. + +END + Right-aligned. (deprecated) + +NUM + Left-aligned. (deprecated) + +START + Left-aligned. (deprecated) diff --git a/docs/api/enum/WdTabLeader.rst b/docs/api/enum/WdTabLeader.rst new file mode 100644 index 000000000..73990eeef --- /dev/null +++ b/docs/api/enum/WdTabLeader.rst @@ -0,0 +1,26 @@ +.. _WdTabLeader: + +``WD_TAB_LEADER`` +================= + +Specifies the character to use as the leader with formatted tabs. + +---- + +SPACES + Spaces. Default. + +DOTS + Dots. + +DASHES + Dashes. + +LINES + Double lines. + +HEAVY + A heavy line. + +MIDDLE_DOT + A vertically-centered dot. diff --git a/docs/api/enum/index.rst b/docs/api/enum/index.rst index 9c7371dcf..f6ba1e261 100644 --- a/docs/api/enum/index.rst +++ b/docs/api/enum/index.rst @@ -15,8 +15,10 @@ can be found here: WdColorIndex WdLineSpacing WdOrientation + WdRowAlignment WdSectionStart WdStyleType - WdRowAlignment + WdTabAlignment + WdTabLeader WdTableDirection WdUnderline diff --git a/docx/enum/text.py b/docx/enum/text.py index 97597eed1..f4111eb92 100644 --- a/docx/enum/text.py +++ b/docx/enum/text.py @@ -195,6 +195,80 @@ class WD_LINE_SPACING(XmlEnumeration): ) +class WD_TAB_ALIGNMENT(XmlEnumeration): + """ + Specifies the tab stop alignment to apply. + """ + + __ms_name__ = 'WdTabAlignment' + + __url__ = 'https://msdn.microsoft.com/EN-US/library/office/ff195609.aspx' + + __members__ = ( + XmlMappedEnumMember( + 'LEFT', 0, 'left', 'Left-aligned.' + ), + XmlMappedEnumMember( + 'CENTER', 1, 'center', 'Center-aligned.' + ), + XmlMappedEnumMember( + 'RIGHT', 2, 'right', 'Right-aligned.' + ), + XmlMappedEnumMember( + 'DECIMAL', 3, 'decimal', 'Decimal-aligned.' + ), + XmlMappedEnumMember( + 'BAR', 4, 'bar', 'Bar-aligned.' + ), + XmlMappedEnumMember( + 'LIST', 6, 'list', 'List-aligned. (deprecated)' + ), + XmlMappedEnumMember( + 'CLEAR', 101, 'clear', 'Clear an inherited tab stop.' + ), + XmlMappedEnumMember( + 'END', 102, 'end', 'Right-aligned. (deprecated)' + ), + XmlMappedEnumMember( + 'NUM', 103, 'num', 'Left-aligned. (deprecated)' + ), + XmlMappedEnumMember( + 'START', 104, 'start', 'Left-aligned. (deprecated)' + ), + ) + + +class WD_TAB_LEADER(XmlEnumeration): + """ + Specifies the character to use as the leader with formatted tabs. + """ + + __ms_name__ = 'WdTabLeader' + + __url__ = 'https://msdn.microsoft.com/en-us/library/office/ff845050.aspx' + + __members__ = ( + XmlMappedEnumMember( + 'SPACES', 0, 'none', 'Spaces. Default.' + ), + XmlMappedEnumMember( + 'DOTS', 1, 'dot', 'Dots.' + ), + XmlMappedEnumMember( + 'DASHES', 2, 'hyphen', 'Dashes.' + ), + XmlMappedEnumMember( + 'LINES', 3, 'underscore', 'Double lines.' + ), + XmlMappedEnumMember( + 'HEAVY', 4, 'heavy', 'A heavy line.' + ), + XmlMappedEnumMember( + 'MIDDLE_DOT', 5, 'middleDot', 'A vertically-centered dot.' + ), + ) + + class WD_UNDERLINE(XmlEnumeration): """ Specifies the style of underline applied to a run of characters. diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index 8c5b2a6ed..528b1eac7 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -181,7 +181,9 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): from .text.paragraph import CT_P register_element_cls('w:p', CT_P) -from .text.parfmt import CT_Ind, CT_Jc, CT_PPr, CT_Spacing, CT_TabStops +from .text.parfmt import ( + CT_Ind, CT_Jc, CT_PPr, CT_Spacing, CT_TabStop, CT_TabStops +) register_element_cls('w:ind', CT_Ind) register_element_cls('w:jc', CT_Jc) register_element_cls('w:keepLines', CT_OnOff) @@ -190,6 +192,7 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:pPr', CT_PPr) register_element_cls('w:pStyle', CT_String) register_element_cls('w:spacing', CT_Spacing) +register_element_cls('w:tab', CT_TabStop) register_element_cls('w:tabs', CT_TabStops) register_element_cls('w:widowControl', CT_OnOff) diff --git a/docx/oxml/text/parfmt.py b/docx/oxml/text/parfmt.py index 78922e231..466b11b1b 100644 --- a/docx/oxml/text/parfmt.py +++ b/docx/oxml/text/parfmt.py @@ -4,7 +4,9 @@ Custom element classes related to paragraph properties (CT_PPr). """ -from ...enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING +from ...enum.text import ( + WD_ALIGN_PARAGRAPH, WD_LINE_SPACING, WD_TAB_ALIGNMENT, WD_TAB_LEADER +) from ...shared import Length from ..simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure from ..xmlchemy import ( @@ -315,8 +317,32 @@ class CT_Spacing(BaseOxmlElement): lineRule = OptionalAttribute('w:lineRule', WD_LINE_SPACING) +class CT_TabStop(BaseOxmlElement): + """ + ```` element, representing an individual tab stop. + """ + val = RequiredAttribute('w:val', WD_TAB_ALIGNMENT) + leader = OptionalAttribute( + 'w:leader', WD_TAB_LEADER, default=WD_TAB_LEADER.SPACES + ) + pos = RequiredAttribute('w:pos', ST_SignedTwipsMeasure) + + class CT_TabStops(BaseOxmlElement): """ ```` element, container for a sorted sequence of tab stops. """ tab = OneOrMore('w:tab', successors=()) + + def insert_tab_in_order(self, pos, align, leader): + """ + Insert a newly created `w:tab` child element in *pos* order. + """ + new_tab = self._new_tab() + new_tab.pos, new_tab.val, new_tab.leader = pos, align, leader + for tab in self.tab_lst: + if new_tab.pos < tab.pos: + tab.addprevious(new_tab) + return new_tab + self.append(new_tab) + return new_tab diff --git a/docx/text/tabstops.py b/docx/text/tabstops.py index a857299c0..29d8aeb66 100644 --- a/docx/text/tabstops.py +++ b/docx/text/tabstops.py @@ -9,6 +9,7 @@ ) from ..shared import ElementProxy +from docx.enum.text import WD_TAB_ALIGNMENT, WD_TAB_LEADER class TabStops(ElementProxy): @@ -25,6 +26,19 @@ def __init__(self, element): super(TabStops, self).__init__(element, None) self._pPr = element + def __delitem__(self, idx): + """ + Remove the tab at offset *idx* in this sequence. + """ + tabs = self._pPr.tabs + try: + tabs.remove(tabs[idx]) + except (AttributeError, IndexError): + raise IndexError('tab index out of range') + + if len(tabs) == 0: + self._pPr.remove(tabs) + def __getitem__(self, idx): """ Enables list-style access by index. @@ -51,6 +65,25 @@ def __len__(self): return 0 return len(tabs.tab_lst) + def add_tab_stop(self, position, alignment=WD_TAB_ALIGNMENT.LEFT, + leader=WD_TAB_LEADER.SPACES): + """ + Add a new tab stop at *position*. Tab alignment defaults to left, but + may be specified by passing a member of the :ref:`WdTabAlignment` + enumeration as *alignment*. An optional leader character can be + specified by passing a member of the :ref:`WdTabLeader` enumeration + as *leader*. + """ + tabs = self._pPr.get_or_add_tabs() + tab = tabs.insert_tab_in_order(position, alignment, leader) + return TabStop(tab) + + def clear_all(self): + """ + Remove all custom tab stops. + """ + self._pPr._remove_tabs() + class TabStop(ElementProxy): """ @@ -63,3 +96,43 @@ class TabStop(ElementProxy): def __init__(self, element): super(TabStop, self).__init__(element, None) self._tab = element + + @property + def alignment(self): + """ + A member of :ref:`WdTabAlignment` specifying the alignment setting + for this tab stop. + """ + return self._tab.val + + @alignment.setter + def alignment(self, value): + self._tab.val = value + + @property + def leader(self): + """ + A member of :ref:`WdTabLeader` specifying a repeating character used + as a "leader", filling in the space spanned by this tab. Assigning + |None| produces the same result as assigning `WD_TAB_LEADER.SPACES`. + """ + return self._tab.leader + + @leader.setter + def leader(self, value): + self._tab.leader = value + + @property + def position(self): + """ + The distance (in EMU) of this tab stop from the inside edge of the + paragraph. May be positive or negative. + """ + return self._tab.pos + + @position.setter + def position(self, value): + if self._tab.pos != value: + self._tab.getparent().insert_tab_in_order(value, self._tab.val, + self._tab.leader) + self._tab.getparent().remove(self._element) diff --git a/features/steps/tabstops.py b/features/steps/tabstops.py index b6396f8bb..637a765ef 100644 --- a/features/steps/tabstops.py +++ b/features/steps/tabstops.py @@ -4,9 +4,11 @@ Step implementations for paragraph-related features """ -from behave import given, then +from behave import given, then, when from docx import Document +from docx.enum.text import WD_TAB_ALIGNMENT, WD_TAB_LEADER +from docx.shared import Inches from docx.text.tabstops import TabStop from helpers import test_docx @@ -22,6 +24,73 @@ def given_a_tab_stops_having_count_tab_stops(context, count): context.tab_stops = paragraph_format.tab_stops +@given('a tab stop 0.5 inches {in_or_out} from the paragraph left edge') +def given_a_tab_stop_inches_from_paragraph_left_edge(context, in_or_out): + tab_idx = {'out': 0, 'in': 1}[in_or_out] + document = Document(test_docx('tab-stops')) + paragraph_format = document.paragraphs[2].paragraph_format + context.tab_stops = paragraph_format.tab_stops + context.tab_stop = paragraph_format.tab_stops[tab_idx] + + +@given('a tab stop having {alignment} alignment') +def given_a_tab_stop_having_alignment_alignment(context, alignment): + tab_idx = {'LEFT': 0, 'CENTER': 1, 'RIGHT': 2}[alignment] + document = Document(test_docx('tab-stops')) + paragraph_format = document.paragraphs[1].paragraph_format + context.tab_stop = paragraph_format.tab_stops[tab_idx] + + +@given('a tab stop having {leader} leader') +def given_a_tab_stop_having_leader_leader(context, leader): + tab_idx = {'no specified': 0, 'a dotted': 2}[leader] + document = Document(test_docx('tab-stops')) + paragraph_format = document.paragraphs[1].paragraph_format + context.tab_stop = paragraph_format.tab_stops[tab_idx] + + +# when ==================================================== + +@when('I add a tab stop') +def when_I_add_a_tab_stop(context): + tab_stops = context.tab_stops + tab_stops.add_tab_stop(Inches(1.75)) + + +@when('I assign {member} to tab_stop.alignment') +def when_I_assign_member_to_tab_stop_alignment(context, member): + value = getattr(WD_TAB_ALIGNMENT, member) + context.tab_stop.alignment = value + + +@when('I assign {member} to tab_stop.leader') +def when_I_assign_member_to_tab_stop_leader(context, member): + value = getattr(WD_TAB_LEADER, member) + context.tab_stop.leader = value + + +@when('I assign {value} to tab_stop.position') +def when_I_assign_value_to_tab_stop_position(context, value): + context.tab_stop.position = int(value) + + +@when('I change the tab at {index} position to {value}') +def when_I_change_the_tab_at_index_position_to_value(context, index, value): + context.tab_stops[int(index)].position = int(value) + + +@when('I call tab_stops.clear_all()') +def when_I_call_tab_stops_clear_all(context): + tab_stops = context.tab_stops + tab_stops.clear_all() + + +@when('I remove a tab stop') +def when_I_remove_a_tab_stop(context): + tab_stops = context.tab_stops + del tab_stops[1] + + # then ===================================================== @then('I can access a tab stop by index') @@ -43,3 +112,45 @@ def then_I_can_iterate_the_TabStops_object(context): def then_len_tab_stops_is_count(context, count): tab_stops = context.tab_stops assert len(tab_stops) == int(count) + + +@then('tab_stop.alignment is {alignment}') +def then_tab_stop_alignment_is_alignment(context, alignment): + expected_value = getattr(WD_TAB_ALIGNMENT, alignment) + tab_stop = context.tab_stop + assert tab_stop.alignment == expected_value + + +@then('tab_stop.leader is {leader}') +def then_tab_stop_leader_is_leader(context, leader): + expected_value = getattr(WD_TAB_LEADER, leader) + tab_stop = context.tab_stop + assert tab_stop.leader == expected_value + + +@then('tab_stop.position is {position}') +def then_tab_stop_position_is_position(context, position): + tab_stop = context.tab_stop + assert tab_stop.position == int(position) + + +# Use this rather than the above method when you change the position, +# as setting position invalidates context.tab_stop +@then('tab_stops at position {index} is {value}') +def then_tab_stops_at_position_index_is_value(context, index, value): + tab_stops = context.tab_stops + assert tab_stops[int(index)].position == int(value) + + +@then('the removed tab stop is no longer present in tab_stops') +def then_the_removed_tab_stop_is_no_longer_present_in_tab_stops(context): + tab_stops = context.tab_stops + assert tab_stops[0].position == Inches(1) + assert tab_stops[1].position == Inches(3) + + +@then('the tab stops are sequenced in position order') +def then_the_tab_stops_are_sequenced_in_position_order(context): + tab_stops = context.tab_stops + for idx in range(len(tab_stops) - 1): + assert tab_stops[idx].position < tab_stops[idx+1].position diff --git a/features/steps/test_files/tab-stops.docx b/features/steps/test_files/tab-stops.docx index a33532779..fa51252ed 100644 Binary files a/features/steps/test_files/tab-stops.docx and b/features/steps/test_files/tab-stops.docx differ diff --git a/features/tab-access-tabs.feature b/features/tab-access-tabs.feature index c517ed012..92adff4a2 100644 --- a/features/tab-access-tabs.feature +++ b/features/tab-access-tabs.feature @@ -18,3 +18,28 @@ Feature: Access TabStop objects Given a tab_stops having 3 tab stops Then I can iterate the TabStops object And I can access a tab stop by index + + + Scenario Outline: TabStops.add_tab_stop() + Given a tab_stops having tab stops + When I add a tab stop + Then len(tab_stops) is + And the tab stops are sequenced in position order + + Examples: tab stop object counts + | count | new-count | + | 0 | 1 | + | 3 | 4 | + + + Scenario: TabStops.__delitem__() + Given a tab_stops having 3 tab stops + When I remove a tab stop + Then len(tab_stops) is 2 + And the removed tab stop is no longer present in tab_stops + + + Scenario: TabStops.clear_all() + Given a tab_stops having 3 tab stops + When I call tab_stops.clear_all() + Then len(tab_stops) is 0 diff --git a/features/tab-tabstop-props.feature b/features/tab-tabstop-props.feature new file mode 100644 index 000000000..66aa89931 --- /dev/null +++ b/features/tab-tabstop-props.feature @@ -0,0 +1,77 @@ +Feature: Tab stop properties + To change the properties of an individual tab stop + As a developer using python-docx + I need a set of read/write properties on TabStop + + + Scenario Outline: Get tab stop position + Given a tab stop 0.5 inches from the paragraph left edge + Then tab_stop.position is + + Examples: tab stop positions + | in-or-out | position | + | in | 457200 | + | out | -457200 | + + + Scenario Outline: Set tab stop position + Given a tab stop 0.5 inches in from the paragraph left edge + When I assign to tab_stop.position + Then tab_stops at position 1 is + + Examples: tab stop positions + | value | + | 228600 | + | -228600 | + + + Scenario Outline: Maintain tab stop position order when changing position + Given a tab_stops having 3 tab stops + When I change the tab at position to + Then the tab stops are sequenced in position order + + Examples: tab stop positions + | index | value | + | 0 | 2285000 | + | 2 | 1371600 | + + Scenario Outline: Get tab stop alignment + Given a tab stop having alignment + Then tab_stop.alignment is + + Examples: tab stop alignments + | alignment | + | LEFT | + | RIGHT | + + + Scenario Outline: Set tab stop alignment + Given a tab stop having alignment + When I assign to tab_stop.alignment + Then tab_stop.alignment is + + Examples: tab stop alignments + | alignment | member | + | LEFT | CENTER | + | RIGHT | LEFT | + + + Scenario Outline: Get tab stop leader + Given a tab stop having leader + Then tab_stop.leader is + + Examples: tab stop leaders + | leader | value | + | no specified | SPACES | + | a dotted | DOTS | + + + Scenario Outline: Set tab stop leader + Given a tab stop having leader + When I assign to tab_stop.leader + Then tab_stop.leader is + + Examples: tab stop leaders + | leader | member | + | no specified | DOTS | + | a dotted | SPACES | diff --git a/tests/text/test_tabstops.py b/tests/text/test_tabstops.py index 05652cdd1..326269d41 100644 --- a/tests/text/test_tabstops.py +++ b/tests/text/test_tabstops.py @@ -9,14 +9,100 @@ absolute_import, division, print_function, unicode_literals ) +from docx.enum.text import WD_TAB_ALIGNMENT, WD_TAB_LEADER +from docx.shared import Twips from docx.text.tabstops import TabStop, TabStops import pytest -from ..unitutil.cxml import element +from ..unitutil.cxml import element, xml from ..unitutil.mock import call, class_mock, instance_mock +class DescribeTabStop(object): + + def it_knows_its_position(self, position_get_fixture): + tab_stop, expected_value = position_get_fixture + assert tab_stop.position == expected_value + + # NOTE: Changing a tab's position invalidates an individual tab, + # so is tested at the TabStops level rather than here. + + def it_knows_its_alignment(self, alignment_get_fixture): + tab_stop, expected_value = alignment_get_fixture + assert tab_stop.alignment == expected_value + + def it_can_change_its_alignment(self, alignment_set_fixture): + tab_stop, value, expected_xml = alignment_set_fixture + tab_stop.alignment = value + assert tab_stop._element.xml == expected_xml + + def it_knows_its_leader(self, leader_get_fixture): + tab_stop, expected_value = leader_get_fixture + assert tab_stop.leader == expected_value + + def it_can_change_its_leader(self, leader_set_fixture): + tab_stop, value, expected_xml = leader_set_fixture + tab_stop.leader = value + assert tab_stop._element.xml == expected_xml + + # fixture -------------------------------------------------------- + + @pytest.fixture(params=[ + ('w:tab{w:val=left}', 'LEFT'), + ('w:tab{w:val=right}', 'RIGHT'), + ]) + def alignment_get_fixture(self, request): + tab_stop_cxml, member = request.param + tab_stop = TabStop(element(tab_stop_cxml)) + expected_value = getattr(WD_TAB_ALIGNMENT, member) + return tab_stop, expected_value + + @pytest.fixture(params=[ + ('w:tab{w:val=left}', 'RIGHT', 'w:tab{w:val=right}'), + ('w:tab{w:val=right}', 'LEFT', 'w:tab{w:val=left}'), + ]) + def alignment_set_fixture(self, request): + tab_stop_cxml, member, expected_cxml = request.param + tab_stop = TabStop(element(tab_stop_cxml)) + expected_xml = xml(expected_cxml) + value = getattr(WD_TAB_ALIGNMENT, member) + return tab_stop, value, expected_xml + + @pytest.fixture(params=[ + ('w:tab', 'SPACES'), + ('w:tab{w:leader=none}', 'SPACES'), + ('w:tab{w:leader=dot}', 'DOTS'), + ]) + def leader_get_fixture(self, request): + tab_stop_cxml, member = request.param + tab_stop = TabStop(element(tab_stop_cxml)) + expected_value = getattr(WD_TAB_LEADER, member) + return tab_stop, expected_value + + @pytest.fixture(params=[ + ('w:tab', 'DOTS', 'w:tab{w:leader=dot}'), + ('w:tab{w:leader=dot}', 'DASHES', 'w:tab{w:leader=hyphen}'), + ('w:tab{w:leader=hyphen}', 'SPACES', 'w:tab'), + ('w:tab{w:leader=hyphen}', None, 'w:tab'), + ('w:tab', 'SPACES', 'w:tab'), + ('w:tab', None, 'w:tab'), + ]) + def leader_set_fixture(self, request): + tab_stop_cxml, new_value, expected_cxml = request.param + tab_stop = TabStop(element(tab_stop_cxml)) + value = ( + None if new_value is None else getattr(WD_TAB_LEADER, new_value) + ) + expected_xml = xml(expected_cxml) + return tab_stop, value, expected_xml + + @pytest.fixture + def position_get_fixture(self, request): + tab_stop = TabStop(element('w:tab{w:pos=720}')) + return tab_stop, Twips(720) + + class DescribeTabStops(object): def it_knows_its_length(self, len_fixture): @@ -45,8 +131,112 @@ def it_raises_on_indexed_access_when_empty(self): with pytest.raises(IndexError): tab_stops[0] + def it_can_add_a_tab_stop(self, add_tab_fixture): + tab_stops, position, kwargs, expected_xml = add_tab_fixture + tab_stops.add_tab_stop(position, **kwargs) + assert tab_stops._element.xml == expected_xml + + def it_can_delete_a_tab_stop(self, del_fixture): + tab_stops, idx, expected_xml = del_fixture + del tab_stops[idx] + assert tab_stops._element.xml == expected_xml + + def it_raises_on_del_idx_invalid(self, del_raises_fixture): + tab_stops, idx = del_raises_fixture + with pytest.raises(IndexError) as exc: + del tab_stops[idx] + assert exc.value.args[0] == 'tab index out of range' + + def it_can_clear_all_its_tab_stops(self, clear_all_fixture): + tab_stops, expected_xml = clear_all_fixture + tab_stops.clear_all() + assert tab_stops._element.xml == expected_xml + + def it_can_change_tab_positions(self, position_set_fixture): + tab_stops, index, value, expected_xml = position_set_fixture + tab_stops[index].position = value + assert tab_stops._element.xml == expected_xml + # fixture -------------------------------------------------------- + @pytest.fixture(params=[ + ('w:pPr/w:tabs/w:tab{w:pos=360,w:val=left}', 0, Twips(720), + 'w:pPr/w:tabs/w:tab{w:pos=720,w:val=left}'), + ('w:pPr/w:tabs/w:tab{w:pos=360,w:val=left}', 0, Twips(-720), + 'w:pPr/w:tabs/w:tab{w:pos=-720,w:val=left}'), + ('w:pPr/w:tabs/(w:tab{w:pos=360,w:val=left},w:tab{w:pos=720,' + 'w:val=center})', 0, Twips(900), 'w:pPr/w:tabs/(w:tab{w:pos=720,' + 'w:val=center},w:tab{w:pos=900,w:val=left})'), + ('w:pPr/w:tabs/(w:tab{w:pos=360,w:val=left},w:tab{w:pos=720,' + 'w:val=center})', 1, Twips(180), 'w:pPr/w:tabs/(w:tab{w:pos=180,' + 'w:val=center},w:tab{w:pos=360,w:val=left})'), + ('w:pPr/w:tabs/(w:tab{w:pos=360,w:val=left},w:tab{w:pos=720,' + 'w:val=center},w:tab{w:pos=900,w:val=left})', 0, Twips(800), + 'w:pPr/w:tabs/(w:tab{w:pos=720,w:val=center},w:tab{w:pos=800,' + 'w:val=left},w:tab{w:pos=900,w:val=left})'), + ]) + def position_set_fixture(self, request): + pPr_cxml, index, value, expected_cxml = request.param + tab_stops = TabStops(element(pPr_cxml)) + expected_xml = xml(expected_cxml) + return tab_stops, index, value, expected_xml + + @pytest.fixture(params=[ + 'w:pPr', + 'w:pPr/w:tabs/w:tab{w:pos=42}', + 'w:pPr/w:tabs/(w:tab{w:pos=24},w:tab{w:pos=42})', + ]) + def clear_all_fixture(self, request): + pPr_cxml = request.param + tab_stops = TabStops(element(pPr_cxml)) + expected_xml = xml('w:pPr') + return tab_stops, expected_xml + + @pytest.fixture(params=[ + ('w:pPr/w:tabs/w:tab{w:pos=42}', 0, + 'w:pPr'), + ('w:pPr/w:tabs/(w:tab{w:pos=24},w:tab{w:pos=42})', 0, + 'w:pPr/w:tabs/w:tab{w:pos=42}'), + ('w:pPr/w:tabs/(w:tab{w:pos=24},w:tab{w:pos=42})', 1, + 'w:pPr/w:tabs/w:tab{w:pos=24}'), + ]) + def del_fixture(self, request): + pPr_cxml, idx, expected_cxml = request.param + tab_stops = TabStops(element(pPr_cxml)) + expected_xml = xml(expected_cxml) + return tab_stops, idx, expected_xml + + @pytest.fixture(params=[ + ('w:pPr', 0), + ('w:pPr/w:tabs/w:tab{w:pos=42}', 1), + ]) + def del_raises_fixture(self, request): + tab_stops_cxml, idx = request.param + tab_stops = TabStops(element(tab_stops_cxml)) + return tab_stops, idx + + @pytest.fixture(params=[ + ('w:pPr', Twips(42), {}, + 'w:pPr/w:tabs/w:tab{w:pos=42,w:val=left}'), + ('w:pPr', Twips(72), {'alignment': WD_TAB_ALIGNMENT.RIGHT}, + 'w:pPr/w:tabs/w:tab{w:pos=72,w:val=right}'), + ('w:pPr', Twips(24), + {'alignment': WD_TAB_ALIGNMENT.CENTER, + 'leader': WD_TAB_LEADER.DOTS}, + 'w:pPr/w:tabs/w:tab{w:pos=24,w:val=center,w:leader=dot}'), + ('w:pPr/w:tabs/w:tab{w:pos=42}', Twips(72), {}, + 'w:pPr/w:tabs/(w:tab{w:pos=42},w:tab{w:pos=72,w:val=left})'), + ('w:pPr/w:tabs/w:tab{w:pos=42}', Twips(24), {}, + 'w:pPr/w:tabs/(w:tab{w:pos=24,w:val=left},w:tab{w:pos=42})'), + ('w:pPr/w:tabs/w:tab{w:pos=42}', Twips(42), {}, + 'w:pPr/w:tabs/(w:tab{w:pos=42},w:tab{w:pos=42,w:val=left})'), + ]) + def add_tab_fixture(self, request): + pPr_cxml, position, kwargs, expected_cxml = request.param + tab_stops = TabStops(element(pPr_cxml)) + expected_xml = xml(expected_cxml) + return tab_stops, position, kwargs, expected_xml + @pytest.fixture(params=[ ('w:pPr/w:tabs/w:tab{w:pos=0}', 0), ('w:pPr/w:tabs/(w:tab{w:pos=1},w:tab{w:pos=2},w:tab{w:pos=3})', 1),