Skip to content

Commit bd58f43

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 89678c5 commit bd58f43

3 files changed

Lines changed: 91 additions & 6 deletions

File tree

docx/bookmark.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,34 @@
66
absolute_import, division, print_function, unicode_literals
77
)
88

9+
from collections import Sequence
910
from itertools import chain
1011

1112
from docx.oxml.ns import qn
1213
from docx.shared import lazyproperty
1314

1415

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

1823
def __init__(self, document_part):
1924
self._document_part = document_part
2025

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

@@ -27,6 +43,13 @@ def _finder(self):
2743
return _DocumentBookmarkFinder(self._document_part)
2844

2945

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

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: 66 additions & 3 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

@@ -24,9 +29,59 @@
2429

2530
class DescribeBookmarks(object):
2631

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

49104
# fixture components ---------------------------------------------
50105

106+
@pytest.fixture
107+
def _Bookmark_(self, request):
108+
return class_mock(request, 'docx.bookmark._Bookmark')
109+
110+
@pytest.fixture
111+
def bookmark_(self, request):
112+
return instance_mock(request, _Bookmark)
113+
51114
@pytest.fixture
52115
def _DocumentBookmarkFinder_(self, request):
53116
return class_mock(request, 'docx.bookmark._DocumentBookmarkFinder')

0 commit comments

Comments
 (0)