GeezerVison is showing github.com — Restart numbering of an ordered list in document. · Issue #25 · python-openxml/python-docx
Restart numbering of an ordered list in document. #25DescriptionWe can easily add ordered list with document.add_paragraph(style='ListNumber') code. But how can we restart its numbering? Reacted by Adam Stauffer, H0r53, palash kulshreshtha, Jesus Alber, csdevignes, pemontto, Peter Zaitcev / [USSX] Hares and Adam Zhu Activitymodified the milestones: This milestone has been deleted, This milestone has been deleted on May 1, 2014modified the milestones: This milestone has been deleted, This milestone has been deleted on May 13, 2014lafolle commented on Aug 6, 2014 ilnurgi commented on Aug 15, 2014 downtown12 commented on Jul 6, 2015 Hi @scanny . I have a problem with workaround way you mentioned in stackoverflow.com. In order to restarting the list numbering, you said one need to locate the numbering definition of the style as well as the abstract numbering definition it points to. I tried but failed to locate these two definitions. Can you explain in detail how they can be located? Cuz I haven't found any documents about them. scanny commented on Jul 6, 2015 Can you link to the SO response? that would save me from taking the time to search it up. downtown12 commented on Jul 7, 2015 scanny commented on Jul 7, 2015 @downtown12 I don't have time to develop step-by-step instructions for you, but if you're looking for the right direction to start looking for yourself, these might be helpful:
- The document object provides access to the numbering part for the document (numbering.xml). This call should get you there:
document.part.numbering_part
- The
NumberingPart object provides access to a sequence of NumberingDefinition objects: numbering_part.numbering_definitions
- The
NumberingDefinitions class looks like where API support ends at the moment. You can get the w:numbering element from it though, which is the parent of all the numbering definitions: numbering_definitions._numbering. From there you'll need to work with lxml calls to get it's children using XPath and so on.
Reacted by soxfmr, Joseph Fox-Rabinovitz and Dmitry Selivanov Reacted by Sean Gillespie downtown12 commented on Jul 7, 2015 Thanks a lot @scanny .Your instructions help me connect the attributes you mentioned above with the elements of numbering.xml. They are pretty helpful. yurac commented on Sep 15, 2015 @downtown12, I also use this feature. I see that all infrastructure is already in python-docx. Enabling it is easy - Below is a patch. I also added accessors for numId, ilvl and ind.left for
convenience
diff --git a/docx/document.py b/docx/document.py
index 655a70e..4a0ec06 100644
--- a/docx/document.py
+++ b/docx/document.py
@@ -51,6 +51,12 @@ class Document(ElementProxy):
paragraph.add_run().add_break(WD_BREAK.PAGE)
return paragraph
+ def get_new_list(self, abstractNumId):
+ """
+ Returns a new numId that references given abstractNumId
+ """
+ return self.numbering.numbering_definitions.add_num(abstractNumId, True)
+
def add_paragraph(self, text='', style=None):
"""
Return a paragraph newly added to the end of the document, populated
@@ -157,6 +163,14 @@ class Document(ElementProxy):
return self._part.styles
@property
+ def numbering(self):
+ """
+ A "Provides access to numbering part
+ """
+ x=self._part.numbering_part
+ return self._part.numbering_part
+
+ @property
def tables(self):
"""
A list of |Table| instances corresponding to the tables in the
diff --git a/docx/oxml/numbering.py b/docx/oxml/numbering.py
index aeedfa9..097f3d6 100644
--- a/docx/oxml/numbering.py
+++ b/docx/oxml/numbering.py
@@ -96,13 +96,15 @@ class CT_Numbering(BaseOxmlElement):
"""
num = ZeroOrMore('w:num', successors=('w:numIdMacAtCleanup',))
- def add_num(self, abstractNum_id):
+ def add_num(self, abstractNum_id, restart=False):
"""
Return a newly added CT_Num (<w:num>) element referencing the
abstract numbering definition identified by *abstractNum_id*.
"""
next_num_id = self._next_numId
num = CT_Num.new(next_num_id, abstractNum_id)
+ if restart:
+ num.add_lvlOverride(ilvl=0).add_startOverride(1)
return self._insert_num(num)
def num_having_numId(self, numId):
diff --git a/docx/parts/numbering.py b/docx/parts/numbering.py
index e324c5a..186c6f8 100644
--- a/docx/parts/numbering.py
+++ b/docx/parts/numbering.py
@@ -43,5 +43,8 @@ class _NumberingDefinitions(object):
super(_NumberingDefinitions, self).__init__()
self._numbering = numbering_elm
+ def add_num(self, abstractNum_id, restart=False):
+ return self._numbering.add_num(abstractNum_id, restart).numId
+
def __len__(self):
return len(self._numbering.num_lst)
downtown12 commented on Sep 18, 2015 @yurac Thanks.
Actually, I solved my issue serveral days ago in another work-around way.
I didn't walk the way that changing the original docx packages but adding some features in the app of myself.
But I wrote much more amount of code than you did.
I read the document_part as well as the numbering_part from the Document object that I was editing.
Firstly I parse the document_part using lxml, once I extract one numbering list paragraph, I create a abstract number node for it in the numbering_part. So that can make sure every list has its numbering restarted.
It seems every road lead to Rome.
Thanks very much for supplying me a new way. removed this from the This milestone has been deleted milestone on Apr 9, 2016akobler commented on Jun 6, 2016 ameily commented on Apr 18, 2017 I need this functionality for a project. It looks like there are two PRs open, #110 and #210, and both are awaiting tests. Can I step in and help and, if so, which PR should I go off of? I personally like the API in #110 better. scanny commented on Apr 21, 2017 Hi Adam, you're more than welcome to contribute. The key thing that stops most folks is getting the tests done. Python folks just don't seem to be test-driven as a whole and this project is strictly test-driven. No commit gets merged without tests. You can look at the commit history to get an idea the granularity and flow of how these feature additions go. Generally look for a commit that starts with 'docs: document analysis for ...' followed by one starting 'acpt: add scenarios for xxx'. Then the implementation follows and then the pattern repeats.
The first step is developing the enhancement proposal, also known as the "analysis page". This is where the API is resolved and so it's a natural first step. It's also separately committable, whether you go on to develop the feature or not. After that, you can pick and choose whatever you want from the existing pull requests to use in yours.
Neither one of the two PRs look like they went in the right direction, which is typically what happens when you don't start with the analysis document. There's a lot to think through and inputs to be collected and understood, like the relevant XML Schema excerpts, Word's own behaviors with respect to lists, what the spec has to say about it (little generally :), and what the MS API is for doing the same from VBA.
Let me know if you need more to go on :) cm-cm-cm-cm commented on Jun 5, 2018 Are there any updates to this? chaithanyaramkumar commented on Jun 20, 2018 how to read bullets or numbering in an existing document
for example
input is
1.apple
2.boy
output is
['1.apple','2.boy'] madphysicist commented on Jun 27, 2018 So I've made a thing that searches the existing abstract numbering schemes for the style of the current paragraph, and sets a reasonable abstract style based on that if possible:
def list_number(doc, par, prev=None, level=None, num=True):
"""
Makes a paragraph into a list item with a specific level and
optional restart.
An attempt will be made to retreive an abstract numbering style that
corresponds to the style of the paragraph. If that is not possible,
the default numbering or bullet style will be used based on the
``num`` parameter.
Parameters
----------
doc : docx.document.Document
The document to add the list into.
par : docx.paragraph.Paragraph
The paragraph to turn into a list item.
prev : docx.paragraph.Paragraph or None
The previous paragraph in the list. If specified, the numbering
and styles will be taken as a continuation of this paragraph.
If omitted, a new numbering scheme will be started.
level : int or None
The level of the paragraph within the outline. If ``prev`` is
set, defaults to the same level as in ``prev``. Otherwise,
defaults to zero.
num : bool
If ``prev`` is :py:obj:`None` and the style of the paragraph
does not correspond to an existing numbering style, this will
determine wether or not the list will be numbered or bulleted.
The result is not guaranteed, but is fairly safe for most Word
templates.
"""
xpath_options = {
True: {'single': 'count(w:lvl)=1 and ', 'level': 0},
False: {'single': '', 'level': level},
}
def style_xpath(prefer_single=True):
"""
The style comes from the outer-scope variable ``par.style.name``.
"""
style = par.style.style_id
return (
'w:abstractNum['
'{single}w:lvl[@w:ilvl="{level}"]/w:pStyle[@w:val="{style}"]'
']/@w:abstractNumId'
).format(style=style, **xpath_options[prefer_single])
def type_xpath(prefer_single=True):
"""
The type is from the outer-scope variable ``num``.
"""
type = 'decimal' if num else 'bullet'
return (
'w:abstractNum['
'{single}w:lvl[@w:ilvl="{level}"]/w:numFmt[@w:val="{type}"]'
']/@w:abstractNumId'
).format(type=type, **xpath_options[prefer_single])
def get_abstract_id():
"""
Select as follows:
1. Match single-level by style (get min ID)
2. Match exact style and level (get min ID)
3. Match single-level decimal/bullet types (get min ID)
4. Match decimal/bullet in requested level (get min ID)
3. 0
"""
for fn in (style_xpath, type_xpath):
for prefer_single in (True, False):
xpath = fn(prefer_single)
ids = numbering.xpath(xpath)
if ids:
return min(int(x) for x in ids)
return 0
if (prev is None or
prev._p.pPr is None or
prev._p.pPr.numPr is None or
prev._p.pPr.numPr.numId is None):
if level is None:
level = 0
numbering = doc.part.numbering_part.numbering_definitions._numbering
# Compute the abstract ID first by style, then by num
anum = get_abstract_id()
# Set the concrete numbering based on the abstract numbering ID
num = numbering.add_num(anum)
# Make sure to override the abstract continuation property
num.add_lvlOverride(ilvl=level).add_startOverride(1)
# Extract the newly-allocated concrete numbering ID
num = num.numId
else:
if level is None:
level = prev._p.pPr.numPr.ilvl.val
# Get the previous concrete numbering ID
num = prev._p.pPr.numPr.numId.val
par._p.get_or_add_pPr().get_or_add_numPr().get_or_add_numId().val = num
par._p.get_or_add_pPr().get_or_add_numPr().get_or_add_ilvl().val = level
This is in no way comprehensive or particularly robust, but it works pretty well for what I am trying to do. Reacted by ocomsoft, Florian Bernard, Aleksandar Bosnjak, magicubedragon, Matt Fisher, meng zhao, Sunghyun, Kyle L. Davis, kgintel, Diogo Fernandes and 3 more Reacted by Florian Bernard, Bob Sutor, Sunghyun, Leon Blum, Kyle L. Davis and Adam Zhu chaithanyaramkumar commented on Jul 3, 2018 chaithanyaramkumar commented on Jul 13, 2018 In a document have one table
we need to read first row is col header is true otherwise its false
also first col is row header otherwise its false
please answer me. nitinkhosla79 commented on Apr 2, 2020 All kudos to @jlovegren0. He added low-level support for numbering styles
We are able to generate multilevel bullet lists now.
PR: #582
We are helping him test his PR. Requesting everyone to please try his api/sample code(in PR comments) and share your success story as well. OR provide feedback.
Thank you all for your support. komawar commented on Apr 2, 2020 Yes, excellent work indeed by @jlovegren0
Nested numbering lists work with correct numbering. komawar commented on Apr 6, 2020 We have a good discussion on PR: #582
This should lay out different possibilities on the implementation of the problem and some of the corner cases/challenges therein
again approbation for @jlovegren0 for all the crafted inputs and workarounds VictorBancho commented on Jul 24, 2023
@yurac Thanks. Actually, I solved my issue serveral days ago in another work-around way. I didn't walk the way that changing the original docx packages but adding some features in the app of myself. But I wrote much more amount of code than you did. I read the document_part as well as the numbering_part from the Document object that I was editing. Firstly I parse the document_part using lxml, once I extract one numbering list paragraph, I create a abstract number node for it in the numbering_part. So that can make sure every list has its numbering restarted. It seems every road lead to Rome. Thanks very much for supplying me a new way.
Hi @downtown12, I know it has been quite a few years since you posted this but I have encountered this issue and wanted to try your solution. Would you mind elaborating on how you parsed the document_part (which property?) using lxml and located the numbering list paragraphs? Furthermoer, how did you manage to create and where/how to insert the abstract number nodes?
I've iterated through the document.part._element, and its children, but that didn't seem to show anything insightful (no list number objects etc even though the document has lists). Wasn't clear if the docs for python-docx also elaborated on how to do this. gookie-dev commented on Aug 21, 2025 I implemented a helper list_number function that rebinds a paragraph to the correct numbering definition.
- If
prev is None, a fresh numId is created from the appropriate abstract numbering definition, and a level override with startOverride(1) is added. This ensures numbering restarts from 1.
- Otherwise, the paragraph inherits the
numId (and optionally the ilvl) from the previous paragraph, so numbering continues within the same list.
- The function supports both numbered and bulleted lists by resolving the correct abstract numbering ID via XPath queries on the numbering part.
Before
from docx import Document
doc = Document()
doc.add_paragraph("List 1:")
for txt in ["L1E1", "L1E2", "L1E3", "L1E4"]:
doc.add_paragraph(txt, style="List Number")
doc.add_paragraph("")
doc.add_paragraph("List 2:")
for txt in ["L2E1", "L2E2", "L2E3", "L2E4"]:
doc.add_paragraph(txt, style="List Number")
doc.save("test.docx")
After
from docx import Document
doc = Document()
doc.add_paragraph("List 1:")
prev = None
for txt in ["L1E1", "L1E2", "L1E3", "L1E4"]:
p = doc.add_paragraph(txt, style="List Number")
list_number(doc, p, prev=prev)
prev = p
doc.add_paragraph("")
doc.add_paragraph("List 2:")
prev = None # restart numbering
for txt in ["L2E1", "L2E2", "L2E3", "L2E4"]:
p = doc.add_paragraph(txt, style="List Number")
list_number(doc, p, prev=prev)
prev = p
doc.save("test.docx")
def list_number(doc, par, prev=None, level=None, numbered=True):
xpath_options = {
True: {"single": "count(w:lvl)=1 and ", "level": 0},
False: {"single": "", "level": level},
}
def style_xpath(prefer_single=True):
style = par.style.style_id
return (
'w:abstractNum['
'{single}w:lvl[@w:ilvl="{level}"]/w:pStyle[@w:val="{style}"]'
']/@w:abstractNumId'
).format(style=style, **xpath_options[prefer_single])
def type_xpath(prefer_single=True):
t = "decimal" if numbered else "bullet"
return (
'w:abstractNum['
'{single}w:lvl[@w:ilvl="{level}"]/w:numFmt[@w:val="{type}"]'
']/@w:abstractNumId'
).format(type=t, **xpath_options[prefer_single])
def get_abstract_id(numbering):
for fn in (style_xpath, type_xpath):
for prefer_single in (True, False):
xp = fn(prefer_single)
ids = numbering.xpath(xp)
if ids:
return min(int(x) for x in ids)
return 0
if (prev is None or
prev._p.pPr is None or
prev._p.pPr.numPr is None or
prev._p.pPr.numPr.numId is None):
if level is None:
level = 0
numbering = doc.part.numbering_part.numbering_definitions._numbering
anum = get_abstract_id(numbering)
num = numbering.add_num(anum)
num.add_lvlOverride(ilvl=level).add_startOverride(1)
num_id = num.numId
else:
if level is None:
level = prev._p.pPr.numPr.ilvl.val
num_id = prev._p.pPr.numPr.numId.val
ppr = par._p.get_or_add_pPr()
numPr = ppr.get_or_add_numPr()
numPr.get_or_add_numId().val = num_id
numPr.get_or_add_ilvl().val = levelReacted by David Drinnan Reacted by Rick Torres Jr., AldyAlfnsyh, Gandarufuuu, Peter Zaitcev / [USSX] Hares and Параноик
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|