Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/api/document.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ The main Document and related objects.
:exclude-members: styles_part


List instances
--------------

.. autoclass:: docx.numbering.ListInstance()
:members: apply, apply_continuation, default_level, levels, restart


|CoreProperties| objects
-------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ User Guide
:maxdepth: 1

user/install
user/lists
user/quickstart
user/documents
user/tables
Expand Down
78 changes: 78 additions & 0 deletions docs/user/lists.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
Lists and numbering
===================

A paragraph style controls appearance. A list instance controls which
paragraphs share a numbering sequence. Create an instance from a paragraph
style that references numbering in your document or template::

from docx import Document

document = Document()
sequence = document.add_list("List Number", start=3)
paragraph = document.add_paragraph("Third item", "List Number")
sequence.apply(paragraph)

continuation = document.add_paragraph("More about the third item", "List Number")
sequence.apply_continuation(continuation)

document.add_paragraph("Intervening text")
sequence.apply(document.add_paragraph("Fourth item", "List Number"))

restarted = sequence.restart(start=1)
restarted.apply(document.add_paragraph("First item of a new list", "List Number"))

Use ``List Bullet`` for bullets. The library never inserts visible number or
bullet text into a run. ``start=0`` is supported. Later paragraph text does
not control the sequence's numbering.

Template numbering
------------------

``Document("brand.docx").add_list("Brand Numbered")`` uses numbering inherited
by that style. The style must already reference a numbering definition.
The library creates a new concrete numbering instance and preserves the
original abstract definition. Each sequence gets a private copy with the same
bullet fonts, indentation, and tabs. This prevents Word from sharing counters
when independent sequences are interleaved.
Existing instance overrides are copied. Numbering-style links are currently
unsupported and raise ``ValueError``. This API does not author numbering
formats or recover a list handle from an existing paragraph.

``apply()`` does not set the paragraph's style. Set it separately, as shown
above. A style is not a list identity. Two calls to ``add_list()`` create
independent sequences even when both use the same style.

Nesting
-------

Use ``sequence.levels`` to inspect available zero-based levels, and
``sequence.apply(paragraph, level=1)`` to select one. Multilevel definitions
use the template's level text and nested restart rules. Returning to an
outer level continues that level's sequence. ``restart()`` creates a new
instance and can select a starting level without changing earlier items.

The built-in ``List Number 2`` and ``List Bullet 2`` styles are independent
single-level definitions with deeper indentation. Their numbering level is
still zero. For nested lists using those styles, create an independent
instance for each nested list. This also supports mixed bullets and numbers.
Do not confuse a visual nesting depth with a template's numbering level.

Continuation paragraphs
-----------------------

``apply_continuation()`` suppresses numbering and removes the first-line
indent while retaining the effective text indentation and tabs. It does not
advance the sequence. Apply the item's style and desired direct formatting
before calling it. This takes a formatting snapshot. Subsequent changes to
the numbering definition do not update the continuation paragraph.

Word has no list-item container. The caller places continuation paragraphs
next to their item. Main-document paragraphs and table-cell paragraphs are
supported. Other stories and cross-document paragraphs are rejected before
mutation. Undefined levels and invalid starting numbers are also rejected.

For a table or another block that cannot receive numbering, call
``sequence.continuation_left_indent(item_paragraph)`` to obtain the same
left indent without changing the item paragraph. For example, assign that
value to ``table.left_indent`` on a left-aligned table. The result is
``None`` when the style and numbering define no explicit left indent.
17 changes: 17 additions & 0 deletions features/num-list-instance.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Feature: Author independent list sequences
In order to control native Word numbering without editing XML
As a document author
I need independent lists and unnumbered continuation paragraphs

Scenario: Start, continue, and restart a numbered list
Given a document with an independent list starting at three
When I add two items with an intervening paragraph
And I restart the list at one
And I save and reopen the list document
Then the items retain their separate numbering identities

Scenario: Add an unnumbered continuation paragraph
Given a document with an independent list starting at three
When I add an item with an unnumbered continuation
And I save and reopen the list document
Then the continuation retains text alignment without a number
58 changes: 58 additions & 0 deletions features/steps/list_instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Acceptance steps for public numbering operations."""

from io import BytesIO

from behave import given, then, when

from docx import Document
from docx.shared import Inches


@given("a document with an independent list starting at three")
def given_independent_list(context):
context.document = Document()
context.sequence = context.document.add_list(start=3)


@when("I add two items with an intervening paragraph")
def when_add_items(context):
context.sequence.apply(context.document.add_paragraph("Third", "List Number"))
context.document.add_paragraph("Intervening text")
context.sequence.apply(context.document.add_paragraph("Fourth", "List Number"))


@when("I restart the list at one")
def when_restart(context):
restarted = context.sequence.restart()
restarted.apply(context.document.add_paragraph("New first", "List Number"))


@when("I add an item with an unnumbered continuation")
def when_add_continuation(context):
context.sequence.apply(context.document.add_paragraph("Third", "List Number"))
context.sequence.apply_continuation(context.document.add_paragraph("More third", "List Number"))


@when("I save and reopen the list document")
def when_roundtrip(context):
stream = BytesIO()
context.document.save(stream)
context.document = Document(stream)


@then("the items retain their separate numbering identities")
def then_numbering(context):
paragraphs = context.document.paragraphs
first = paragraphs[0]._p.pPr.numPr.numId.val
assert paragraphs[2]._p.pPr.numPr.numId.val == first
assert paragraphs[3]._p.pPr.numPr.numId.val != first
numbering = context.document.part.numbering_part.element
assert numbering.num_having_numId(first).lvlOverride_lst[0].startOverride.val == 3


@then("the continuation retains text alignment without a number")
def then_continuation(context):
paragraph = context.document.paragraphs[1]
assert paragraph._p.pPr.numPr.numId.val == 0
assert paragraph.paragraph_format.left_indent == Inches(0.25)
assert paragraph.paragraph_format.first_line_indent == 0
20 changes: 20 additions & 0 deletions src/docx/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from docx.blkcntnr import BlockItemContainer
from docx.enum.section import WD_SECTION
from docx.enum.text import WD_BREAK
from docx.numbering import ListInstance
from docx.section import Section, Sections
from docx.shared import ElementProxy, Emu, Inches, Length
from docx.text.run import Run
Expand Down Expand Up @@ -38,6 +39,25 @@ def __init__(self, element: CT_Document, part: DocumentPart):
self._part = part
self.__body = None

def add_list(
self,
style: str | ParagraphStyle = "List Number",
*,
start: int = 1,
level: int | None = None,
) -> ListInstance:
"""Create an independent list using the numbering referenced by `style`.

`start` selects the first number, including zero. `level` selects a
zero-based level defined by the template. Omit it to use the style's
associated level. The style must belong to this document and reference
an existing numbering definition. Numbering-style links are unsupported.

This adds a numbering instance but no paragraphs. Use its ``apply()``
method on paragraphs to number them. Set their styles separately.
"""
return ListInstance._from_style(self, style, start, level)

def add_comment(
self,
runs: Run | Sequence[Run],
Expand Down
Loading