Skip to content

Commit 425245c

Browse files
Benjamin Toornstrascanny
authored andcommitted
bmk: add Bookmarks.__getitem__() and .__iter__()
Adding .__iter__() is not strictly required to enable iteration, but it improves the performance of iteration significantly by avoiding the default implementation which would repeatedly parse the bookmark pairs in the document.
1 parent 80a7ff4 commit 425245c

3 files changed

Lines changed: 99 additions & 10 deletions

File tree

docx/bookmark.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,34 @@
44

55
from __future__ import absolute_import, division, print_function, unicode_literals
66

7+
from collections import Sequence
78
from itertools import chain
89

910
from docx.oxml.ns import qn
1011
from docx.shared import lazyproperty
1112

1213

13-
class Bookmarks(object):
14-
"""Sequence of |Bookmark| objects."""
14+
class Bookmarks(Sequence):
15+
"""Sequence of |Bookmark| objects.
16+
17+
Supports indexed access (including slices), `len()`, and iteration. Iteration will
18+
perform significantly better than repeated indexed access.
19+
"""
1520

1621
def __init__(self, document_part):
1722
self._document_part = document_part
1823

24+
def __getitem__(self, idx):
25+
"""Supports indexed and sliced access."""
26+
bookmark_pairs = self._finder.bookmark_pairs
27+
if isinstance(idx, slice):
28+
return [_Bookmark(pair) for pair in bookmark_pairs[idx]]
29+
return _Bookmark(bookmark_pairs[idx])
30+
31+
def __iter__(self):
32+
"""Supports iteration."""
33+
return (_Bookmark(pair) for pair in self._finder.bookmark_pairs)
34+
1935
def __len__(self):
2036
return len(self._finder.bookmark_pairs)
2137

@@ -25,6 +41,13 @@ def _finder(self):
2541
return _DocumentBookmarkFinder(self._document_part)
2642

2743

44+
class _Bookmark(object):
45+
"""Proxy for a (w:bookmarkStart, w:bookmarkEnd) element pair."""
46+
47+
def __init__(self, bookmark_pair):
48+
self._bookmarkStart, self._bookmarkEnd = bookmark_pair
49+
50+
2851
class _DocumentBookmarkFinder(object):
2952
"""Provides access to bookmark oxml elements in an overall document."""
3053

features/bmk-bookmarks.feature

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ Feature: Access a bookmark
44
I need sequence operations on Bookmarks
55

66

7-
@wip
87
Scenario: Bookmarks is a sequence
98
Given a Bookmarks object of length 5 as bookmarks
109
Then len(bookmarks) == 5

tests/test_bookmark.py

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66

77
import pytest
88

9-
from docx.bookmark import Bookmarks, _DocumentBookmarkFinder, _PartBookmarkFinder
9+
from docx.bookmark import (
10+
_Bookmark,
11+
Bookmarks,
12+
_DocumentBookmarkFinder,
13+
_PartBookmarkFinder,
14+
)
1015
from docx.opc.part import Part, XmlPart
1116
from docx.parts.document import DocumentPart
1217

@@ -23,6 +28,60 @@
2328

2429

2530
class DescribeBookmarks(object):
31+
"""Unit-test suite for `docx.bookmark.Bookmarks` object."""
32+
33+
def it_provides_access_to_bookmarks_by_index(
34+
self, _finder_prop_, finder_, _Bookmark_, bookmark_
35+
):
36+
bookmarkStarts = tuple(element("w:bookmarkStart") for _ in range(3))
37+
bookmarkEnds = tuple(element("w:bookmarkEnd") for _ in range(3))
38+
_finder_prop_.return_value = finder_
39+
finder_.bookmark_pairs = zip(bookmarkStarts, bookmarkEnds)
40+
_Bookmark_.return_value = bookmark_
41+
bookmarks = Bookmarks(None)
42+
43+
bookmark = bookmarks[1]
44+
45+
_Bookmark_.assert_called_once_with((bookmarkStarts[1], bookmarkEnds[1]))
46+
assert bookmark == bookmark_
47+
48+
def it_provides_access_to_bookmarks_by_slice(
49+
self, _finder_prop_, finder_, _Bookmark_, bookmark_
50+
):
51+
bookmarkStarts = tuple(element("w:bookmarkStart") for _ in range(4))
52+
bookmarkEnds = tuple(element("w:bookmarkEnd") for _ in range(4))
53+
_finder_prop_.return_value = finder_
54+
finder_.bookmark_pairs = zip(bookmarkStarts, bookmarkEnds)
55+
_Bookmark_.return_value = bookmark_
56+
bookmarks = Bookmarks(None)
57+
58+
bookmarks_slice = bookmarks[1:3]
59+
60+
assert _Bookmark_.call_args_list == [
61+
call((bookmarkStarts[1], bookmarkEnds[1])),
62+
call((bookmarkStarts[2], bookmarkEnds[2])),
63+
]
64+
assert bookmarks_slice == [bookmark_, bookmark_]
65+
66+
def it_can_iterate_its_bookmarks(
67+
self, _finder_prop_, finder_, _Bookmark_, bookmark_
68+
):
69+
bookmarkStarts = tuple(element("w:bookmarkStart") for _ in range(3))
70+
bookmarkEnds = tuple(element("w:bookmarkEnd") for _ in range(3))
71+
_finder_prop_.return_value = finder_
72+
finder_.bookmark_pairs = zip(bookmarkStarts, bookmarkEnds)
73+
_Bookmark_.return_value = bookmark_
74+
bookmarks = Bookmarks(None)
75+
76+
_bookmarks = list(b for b in bookmarks)
77+
78+
assert _Bookmark_.call_args_list == [
79+
call((bookmarkStarts[0], bookmarkEnds[0])),
80+
call((bookmarkStarts[1], bookmarkEnds[1])),
81+
call((bookmarkStarts[2], bookmarkEnds[2])),
82+
]
83+
assert _bookmarks == [bookmark_, bookmark_, bookmark_]
84+
2685
def it_knows_how_many_bookmarks_the_document_contains(self, _finder_prop_, finder_):
2786
_finder_prop_.return_value = finder_
2887
finder_.bookmark_pairs = tuple((1, 2) for _ in range(42))
@@ -45,6 +104,14 @@ def it_provides_access_to_its_bookmark_finder_to_help(
45104

46105
# fixture components ---------------------------------------------
47106

107+
@pytest.fixture
108+
def _Bookmark_(self, request):
109+
return class_mock(request, "docx.bookmark._Bookmark")
110+
111+
@pytest.fixture
112+
def bookmark_(self, request):
113+
return instance_mock(request, _Bookmark)
114+
48115
@pytest.fixture
49116
def _DocumentBookmarkFinder_(self, request):
50117
return class_mock(request, "docx.bookmark._DocumentBookmarkFinder")
@@ -125,9 +192,7 @@ def it_provides_an_iter_start_end_pairs_interface_method(
125192
assert pairs == _iter_start_end_pairs_.return_value
126193

127194
def it_gathers_all_the_bookmark_start_and_end_elements_to_help(self, part_):
128-
body = element(
129-
"w:body/(w:bookmarkStart,w:p,w:bookmarkEnd,w:p,w:bookmarkStart)"
130-
)
195+
body = element("w:body/(w:bookmarkStart,w:p,w:bookmarkEnd,w:p,w:bookmarkStart)")
131196
part_.element = body
132197
finder = _PartBookmarkFinder(part_)
133198

@@ -190,7 +255,9 @@ def it_iterates_bookmarkStart_elements_to_help(self, _all_starts_and_ends_prop_)
190255
starts = list(finder._iter_starts())
191256

192257
assert starts == [
193-
(0, starts_and_ends[0]), (2, starts_and_ends[2]), (4, starts_and_ends[4])
258+
(0, starts_and_ends[0]),
259+
(2, starts_and_ends[2]),
260+
(4, starts_and_ends[4]),
194261
]
195262

196263
def it_finds_the_matching_end_for_a_start_to_help(
@@ -279,7 +346,7 @@ def name_used_fixture(self, request):
279346

280347
@pytest.fixture
281348
def _all_starts_and_ends_prop_(self, request):
282-
return property_mock(request, _PartBookmarkFinder, '_all_starts_and_ends')
349+
return property_mock(request, _PartBookmarkFinder, "_all_starts_and_ends")
283350

284351
@pytest.fixture
285352
def _init_(self, request):
@@ -303,7 +370,7 @@ def _name_already_used_(self, request):
303370

304371
@pytest.fixture
305372
def _names_so_far_prop_(self, request):
306-
return property_mock(request, _PartBookmarkFinder, '_names_so_far')
373+
return property_mock(request, _PartBookmarkFinder, "_names_so_far")
307374

308375
@pytest.fixture
309376
def names_so_far_(self, request):

0 commit comments

Comments
 (0)