From 830c067242a687919d58e199b0062b4d5b3ae18d Mon Sep 17 00:00:00 2001 From: Mohamed Attahri Date: Thu, 27 Nov 2014 17:31:59 +0000 Subject: [PATCH 1/5] Added new add_list method to enhance list support. Added support for multiple numbered/bullet lists with a new add_list method. ListParagraph is a proxy container that supports adding Paragraph instances sharing the same numId. --- docx/api.py | 17 +++++++++++++ docx/blkcntnr.py | 34 ++++++++++++++++++++++++++ docx/list.py | 55 ++++++++++++++++++++++++++++++++++++++++++ docx/parts/document.py | 17 +++++++++++++ docx/text.py | 35 +++++++++++++++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 docx/list.py diff --git a/docx/api.py b/docx/api.py index c1ac093b7..8890ed382 100644 --- a/docx/api.py +++ b/docx/api.py @@ -71,6 +71,13 @@ def add_paragraph(self, text='', style=None): """ return self._document_part.add_paragraph(text, style) + def add_list(self, style='ListParagraph', level=0): + """ + Return a list paragraph newly added to the end of the document, having + paragraph style *style* and indentation level *level*. + """ + return self._document_part.add_list(style=style, level=level) + def add_picture(self, image_path_or_stream, width=None, height=None): """ Return a new picture shape added in its own paragraph at the end of @@ -137,6 +144,16 @@ def paragraphs(self): """ return self._document_part.paragraphs + @property + def lists(self): + """ + A list of |ListParagraph| instances corresponding to the paragraphs + grouped in lists, in document order. Note that paragraphs within + revision marks such as ```` or ```` do not appear in this + list. + """ + return self._document_part.lists + def save(self, path_or_stream): """ Save this document to *path_or_stream*, which can be either a path to diff --git a/docx/blkcntnr.py b/docx/blkcntnr.py index b11f3a50d..006a8d29c 100644 --- a/docx/blkcntnr.py +++ b/docx/blkcntnr.py @@ -8,8 +8,11 @@ from __future__ import absolute_import, print_function +import sys +import random from .shared import Parented from .text import Paragraph +from .list import ListParagraph class BlockItemContainer(Parented): @@ -23,6 +26,15 @@ def __init__(self, element, parent): super(BlockItemContainer, self).__init__(parent) self._element = element + def generate_numId(self): + """ + Generate a unique numId value on this container. + """ + while True: + numId = random.randint(0, 999999) + if not len(self._element.xpath("//w:numId[@w:val='%s']" % numId)): + return numId + def add_paragraph(self, text='', style=None): """ Return a paragraph newly added to the end of the content in this @@ -52,6 +64,19 @@ def add_table(self, rows, cols): table.add_row() return table + def add_list(self, style=None, level=0): + """ + Return a list paragraph newly added to the end of the content in this + container, having a paragraph style *style* and an indentation level + *level*. + """ + return ListParagraph( + self, + numId=self.generate_numId(), + style=style, + level=level, + ) + @property def paragraphs(self): """ @@ -60,6 +85,15 @@ def paragraphs(self): """ return [Paragraph(p, self) for p in self._element.p_lst] + @property + def lists(self): + """ + A list containing the paragraphs grouped in lists in this container, + in document order. Read-only. + """ + nums = [paragraph.numId for paragraph in self.paragraphs] + return [ListParagraph(self, numId) for numId in set(filter(bool, nums))] + @property def tables(self): """ diff --git a/docx/list.py b/docx/list.py new file mode 100644 index 000000000..09c4360d9 --- /dev/null +++ b/docx/list.py @@ -0,0 +1,55 @@ +# encoding: utf-8 + +""" +The |ListParagraph| object and related proxy classes. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import random +from .text import Paragraph + + +class ListParagraph(object): + """ + Proxy object for controlling a set of ```` grouped together in a list. + """ + def __init__(self, parent, numId=0, style=None, level=0): + self._parent = parent + self.numId = numId + self.level = level + self.style = style + + def add_item(self, text=None, style=None): + """ + Add a paragraph item to the current list, having text set to *text* and + a paragraph style *style* + """ + item = self._parent.add_paragraph(text, style=style) + item.level = self.level + item.numId = self.numId + return item + + def add_list(self, style=None): + """ + Add a list indented one level below the current one, having a paragraph + style *style*. Note that the document will only be altered once the + first item has been added to the list. + """ + return ListParagraph( + self._parent, + numId=self._parent.generate_numId(), + style=style if style is not None else self.style, + level=self.level+1, + ) + + @property + def items(self): + """ + Sequence of |Paragraph| instances corresponding to the item elements + in this list paragraph. + """ + return [paragraph for paragraph in self._parent.paragraphs + if paragraph.numId == self.numId] diff --git a/docx/parts/document.py b/docx/parts/document.py index e7ff08e8b..be2ea4596 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -45,6 +45,13 @@ def add_table(self, rows, cols): """ return self.body.add_table(rows, cols) + def add_list(self, style=None, level=0): + """ + Return a paragraph newly added to the end of the body content and + formatted to contain a list. + """ + return self.body.add_list(style=style, level=level) + @lazyproperty def body(self): """ @@ -95,6 +102,16 @@ def paragraphs(self): """ return self.body.paragraphs + @property + def lists(self): + """ + A list of |ListParagraph| instances corresponding to the paragraphs in + parts of lists in the document, in document order. + Note that list paragraphs within revision marks such as inserted or + deleted do not appear in this list. + """ + return self.body.lists + @lazyproperty def sections(self): """ diff --git a/docx/text.py b/docx/text.py index 0c551beeb..dc8c50deb 100644 --- a/docx/text.py +++ b/docx/text.py @@ -63,6 +63,13 @@ def __init__(self, p, parent): super(Paragraph, self).__init__(parent) self._p = p + # XPath: w:p/w:pPr/w:numPr + numPr = p.get_or_add_pPr().get_or_add_numPr() + # XPath: w:p/w:pPr/w:numPr/w:ilvl + self._ilvl = numPr.get_or_add_ilvl() + # XPath: w:p/w:pPr/w:numPr/w:numId + self._numId = numPr.get_or_add_numId() + def add_run(self, text=None, style=None): """ Append a run to this paragraph containing *text* and having character @@ -80,6 +87,34 @@ def add_run(self, text=None, style=None): run.style = style return run + @property + def numId(self): + """ + Return the numId of the parent list. + """ + return self._numId.val + + @numId.setter + def numId(self, numId): + """ + Set the numId of the parent list. + """ + self._numId.val = numId + + @property + def level(self): + """ + Return the indentation level of the parent list. + """ + return self._ilvl.val + + @level.setter + def level(self, lvl): + """ + Set the indentation level of the parent list. + """ + self._ilvl.val = lvl + @property def alignment(self): """ From 910e597fce8a6962ddd80752b6822a97da457b91 Mon Sep 17 00:00:00 2001 From: Mohamed Attahri Date: Thu, 27 Nov 2014 18:12:00 +0000 Subject: [PATCH 2/5] Now passes travis tests. Newly added properties were generating empty XML tags in the document XML. --- docx/blkcntnr.py | 5 +++-- docx/oxml/text.py | 9 ++++++++- docx/text.py | 27 ++++++++++++++++----------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/docx/blkcntnr.py b/docx/blkcntnr.py index 006a8d29c..576745f49 100644 --- a/docx/blkcntnr.py +++ b/docx/blkcntnr.py @@ -91,8 +91,9 @@ def lists(self): A list containing the paragraphs grouped in lists in this container, in document order. Read-only. """ - nums = [paragraph.numId for paragraph in self.paragraphs] - return [ListParagraph(self, numId) for numId in set(filter(bool, nums))] + nums = [paragraph.numId for paragraph in self.paragraphs + if paragraph.numId is not None] + return [ListParagraph(self, numId) for numId in set(nums)] @property def tables(self): diff --git a/docx/oxml/text.py b/docx/oxml/text.py index 9fdd1d64b..02ced33d1 100644 --- a/docx/oxml/text.py +++ b/docx/oxml/text.py @@ -24,7 +24,7 @@ class CT_Br(BaseOxmlElement): class CT_Jc(BaseOxmlElement): """ - ```` element, specifying paragraph justification. + ```` element, specifying paragraph justification. """ val = RequiredAttribute('w:val', WD_ALIGN_PARAGRAPH) @@ -166,6 +166,13 @@ def style(self, style): pStyle.val = style +class CT_NumPr(BaseOxmlElement): + """ + ```` element, containing properties useful for lists. + """ + numId = ZeroOrOne('w:numId') + ilvl = ZeroOrOne('w:ilvl') + class CT_R(BaseOxmlElement): """ ```` element, containing the properties and text for a run. diff --git a/docx/text.py b/docx/text.py index dc8c50deb..b3df7b503 100644 --- a/docx/text.py +++ b/docx/text.py @@ -63,13 +63,6 @@ def __init__(self, p, parent): super(Paragraph, self).__init__(parent) self._p = p - # XPath: w:p/w:pPr/w:numPr - numPr = p.get_or_add_pPr().get_or_add_numPr() - # XPath: w:p/w:pPr/w:numPr/w:ilvl - self._ilvl = numPr.get_or_add_ilvl() - # XPath: w:p/w:pPr/w:numPr/w:numId - self._numId = numPr.get_or_add_numId() - def add_run(self, text=None, style=None): """ Append a run to this paragraph containing *text* and having character @@ -92,28 +85,40 @@ def numId(self): """ Return the numId of the parent list. """ - return self._numId.val + if self._p.pPr is None: + return None + if self._p.pPr.numPr is None: + return None + if self._p.pPr.numPr.numId is None: + return None + return self._p.pPr.numPr.numId.val @numId.setter def numId(self, numId): """ Set the numId of the parent list. """ - self._numId.val = numId + self._p.get_or_add_pPr().get_or_add_numPr().get_or_add_numId().val = numId @property def level(self): """ Return the indentation level of the parent list. """ - return self._ilvl.val + if self._p.pPr is None: + return None + if self._p.pPr.numPr is None: + return None + if self._p.pPr.numPr.ilvl is None: + return None + return self._p.pPr.numPr.ilvl.val @level.setter def level(self, lvl): """ Set the indentation level of the parent list. """ - self._ilvl.val = lvl + self._p.get_or_add_pPr().get_or_add_numPr().get_or_add_ilvl().val = lvl @property def alignment(self): From 7305e1d6b5d7459195fc0c8ecb1729980863e32c Mon Sep 17 00:00:00 2001 From: Mohamed Attahri Date: Thu, 27 Nov 2014 21:34:16 +0000 Subject: [PATCH 3/5] Fixed documentation modified by mistake. --- docx/oxml/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docx/oxml/text.py b/docx/oxml/text.py index 02ced33d1..0870fea19 100644 --- a/docx/oxml/text.py +++ b/docx/oxml/text.py @@ -24,7 +24,7 @@ class CT_Br(BaseOxmlElement): class CT_Jc(BaseOxmlElement): """ - ```` element, specifying paragraph justification. + ```` element, specifying paragraph justification. """ val = RequiredAttribute('w:val', WD_ALIGN_PARAGRAPH) From e7b1551b7b6d824300a91ec6b72752e3f56343a3 Mon Sep 17 00:00:00 2001 From: Mohamed Attahri Date: Thu, 27 Nov 2014 21:40:38 +0000 Subject: [PATCH 4/5] Improved comments --- docx/parts/document.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docx/parts/document.py b/docx/parts/document.py index be2ea4596..010172799 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -47,8 +47,10 @@ def add_table(self, rows, cols): def add_list(self, style=None, level=0): """ - Return a paragraph newly added to the end of the body content and - formatted to contain a list. + Return a helper that provides methods to add paragraphs to the end of + the body content, grouped together as a list. The paragraphs will by + default have their paragraph style set to *style*, and their indentation + level set to *level*. """ return self.body.add_list(style=style, level=level) @@ -105,8 +107,8 @@ def paragraphs(self): @property def lists(self): """ - A list of |ListParagraph| instances corresponding to the paragraphs in - parts of lists in the document, in document order. + A list of |ListParagraph| instances corresponding to lists formed by + paragraphs sharing the same numId in the document, in document order. Note that list paragraphs within revision marks such as inserted or deleted do not appear in this list. """ From 6b97f80ef3632de57166e9857b9b578cd5abcfa5 Mon Sep 17 00:00:00 2001 From: Mohamed Attahri Date: Thu, 27 Nov 2014 21:45:07 +0000 Subject: [PATCH 5/5] More doc and comment fixes. --- docx/api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docx/api.py b/docx/api.py index 8890ed382..21de6116f 100644 --- a/docx/api.py +++ b/docx/api.py @@ -73,8 +73,9 @@ def add_paragraph(self, text='', style=None): def add_list(self, style='ListParagraph', level=0): """ - Return a list paragraph newly added to the end of the document, having - paragraph style *style* and indentation level *level*. + Return a helper that implements methods to create a list formed by + paragraphs sharing the same numId, added to the end of the document, + having paragraph style *style* and indentation level *level*. """ return self._document_part.add_list(style=style, level=level)