sphinx-doc / sphinx-doc/sphinx

Is the "tbody" specificity required for LaTeXFootnoteVisitor.depart_table?

Open
#11,751 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
8k
Forks
2.6k
PR merge metrics
No merged PRs in 30d

Description

Describe the bug

Sphinx Version: 7.1.2
Command Issued: sphinx-build -b latex -d build/doctrees source build/latex (to generate a .tex file which is to be later converted to a PDF)

In my project's documentation, we have custom "longtable" class tables that don't include "tbody" tags.

When Sphinx runs the LaTeXFootnoteTransform and subsequently employs the LaTeXFootnoteVisitor.depart_table method to end the visit, it performs the following: tbody = next(node.findall(nodes.tbody)). Given that there are no "tbody" tags for our custom tables, the next function within the LaTeXFootnoteVisitor.depart_table attempts to operate on an empty iterator, issues a StopIteration error, and all Sphinx document processing dies. Our custom node processing function, process_required_equipment_list_nodes, is never invoked because emitting event: 'doctree-resolved' is never reached.

If I comment out app.add_post_transform(LaTeXFootnoteTransform) from the setup function in builders/latex/transforms.py, I'm able to bypass that transform. Our custom node processing function, process_required_equipment_list_nodes, is invoked since emitting event: 'doctree-resolved' is reached in this case. Sphinx processing is then able to complete, and the desired LaTeX file is generated with the correct content in the custom tables. The resulting PDF file that is converted from the output .tex file looks as expected.

Given the above information, is the "tbody" specificity required?

An example of the LaTeX for one of those custom tables is provided below. It was copied directly from the .tex file generated by Sphinx.

\begin{savenotes}
\sphinxatlongtablestart
\sphinxthistablewithglobalstyle
\makeatletter
\LTleft @totalleftmargin plus1fill
\LTright\dimexpr\columnwidth-@totalleftmargin-\linewidth\relax plus1fill
\makeatother
\begin{longtable}{{3}{\X{1}{3}}}
\sphinxthelongtablecaptionisattop
\caption{Required Equipment\strut}\label{\detokenize{front_matter/required_materials:id3}}\
[\sphinxlongtablecapskipadjust]
\sphinxtoprule
\sphinxstyletheadfamily \sphinxstylestrong{Part Number}&\sphinxstyletheadfamily \sphinxstylestrong{Title}&\sphinxstyletheadfamily \sphinxstylestrong{Referenced Sections}\
\sphinxmidrule
\endfirsthead

\multicolumn{3}{c}{\sphinxnorowcolor
\makebox[0pt]{\sphinxtablecontinued{\tablename\ \thetable{} \textendash{} continued from previous page}}%
}\
\sphinxtoprule
\sphinxstyletheadfamily \sphinxstylestrong{Part Number}&\sphinxstyletheadfamily \sphinxstylestrong{Title}&\sphinxstyletheadfamily \sphinxstylestrong{Referenced Sections}\
\sphinxmidrule
\endhead

\sphinxbottomrule
\multicolumn{3}{r}{\sphinxnorowcolor
\makebox[0pt][r]{\sphinxtablecontinued{continues on next page}}%
}\
\endfoot

\endlastfoot
\sphinxtableatstartofbodyhook

\sphinxAtStartPar

< ...Rows are filled in here...>

\
\sphinxbottomrule
\end{longtable}
\sphinxtableafterendhook
\sphinxatlongtableend
\end{savenotes}

How to Reproduce

Below is the definition (within one of our .rst files) of the custom table from above.

.. required_equipment_list::
:widths: 30 30 30
:include_references: true
:name: Required Equipment Table

Below is some of the pertinent Python code for that custom table.

def setup(app):

app.add_config_value('required_equipment_list', 'configuration_files/ifc_baseline/equipment.yml', 'env')
app.add_node(required_equipment_list,
             html=(visit_pass, depart_pass),
             latex=(visit_pass, depart_pass),
             text=(visit_pass, depart_pass))
app.add_directive('required_equipment_list', RequiredEquipmentListDirective)
app.add_role('equipment', equipment_role)
app.connect('doctree-resolved', process_required_equipment_list_nodes)

class RequiredEquipmentListDirective(Table):

required_arguments = 0
optional_arguments = 1
has_content = False

option_spec = {'include_references': directives.unchanged,
               'include_attributes': directives.unchanged,
               'exact_match_only': directives.unchanged,
               'class': directives.class_option,
               'name': directives.unchanged,
               'align': align,
               'widths': directives.value_or(('auto', 'grid'),
                                             directives.positive_int_list)}

def run(self):
    env = self.state.document.settings.env
    app = env.app
    config = app.config

    if not self.arguments:
        self.arguments = ['Required Equipment']
    title, messages = self.make_title()
    env.required_equipment_title_node = title

    rel = required_equipment_list()
    rel['options'] = self.options
    table = nodes.table('', classes=['longtable'])
    table.insert(0, title)
    rel['header'] = [nodes.strong('Part Number', 'Part Number'),
                     nodes.strong('Title', 'Title')]
    num_cols = len(rel['header'])

    rel['attribute_cols'] = []
    if 'include_attributes' in self.options:
        for attribute in self.options.get('include_attributes').split(','):
            attr_pair = attribute.strip().split(':')
            if len(attr_pair) == 2:
                attr_title = attr_pair[0].strip()
                attr_name = attr_pair[1].strip()
            else:
                attr_title = attr_name = attr_pair[0].strip()

            rel['header'] += [nodes.strong(str(attr_title), str(attr_title))]
            rel['attribute_cols'] += [attr_name]
            num_cols += 1

    if self.options.get('include_references', '').lower() in ('yes', '1', 'true'):
        rel['include_references'] = True
        num_cols += 1
        rel['header'] += [nodes.strong('Referenced Sections', 'Referenced Sections')]
    else:
        rel['include_references'] = False

    rel['num_cols'] = num_cols

    table += rel
    return [table]

def process_required_equipment_list_nodes(app, doctree, fromdocname):

global relations
global included_docs

env = app.builder.env

if not relations:
    relations = get_doc_relations(env)
    included_docs = relations['included_docs']

for table in doctree.traverse(required_equipment_list):
    content = []
    header = table['header']
    tgroup = nodes.tgroup(cols=len(table['header']))
    table += tgroup

    col_widths = get_column_widths(table, table['num_cols'], env, fromdocname)
    for col_width in col_widths:
        colspec = nodes.colspec()
        if col_width is not None:
            colspec.attributes['colwidth'] = col_width
        tgroup += colspec

    thead = nodes.thead()
    tgroup += thead
    thead += create_table_row(header)
    tbody = nodes.tbody()
    tgroup += tbody

    count = 0

    required_equipment = {}

    for docname in included_docs:
        for entry in [i for i in env.all_equipment if i['docname'] == docname]:
            if entry['pn'] not in required_equipment:
                required_equipment[entry['pn']] = {
                    'referenced_sections': [],
                    'info': entry['equipment_info'],
                }
            section_info = get_section_containing_node(entry['target'])
            section_number = get_section_number(env, docname, section_info['section_id'])
            section_info['section_number'] = section_number
            section_info['docname'] = docname
            section_info['targetid'] = entry['targetid']
            required_equipment[entry['pn']]['referenced_sections'].append(section_info)
    count = 0
    for pn in sorted(required_equipment):
        row = [pn, required_equipment[pn]['info']['title']]
        for attr in table['attribute_cols']:
            row += [str(env.equipment_info[pn].get(attr, ''))]
        if table['include_references']:
            paragraphs = []
            for reference in sorted(required_equipment[pn]['referenced_sections'], key=lambda x: x['section_number']['section_num_list']):
                paragraph = nodes.paragraph()
                new_link = create_link(env, app, fromdocname, reference['docname'], reference['targetid'], reference['section_number']['section_num_str'])
                paragraph += new_link
                paragraphs.append(paragraph)
            row.append(paragraphs)
        tbody.append(create_table_row(row))
        count += 1
    if count == 0:
        tbody.append(create_table_row(['N/A'] * table['num_cols']))
    table.replace_self([table])
Environment Information
We are running Gitlab with a runner built using the "sphinxdoc/sphinx-latexpdf" image from Docker Hub.  When the StopIteration issue is encountered and Sphinx dies, Gitlab automatically performs cleanup and removes the container (i.e., the runner).  Attempting to execute "sphinx-build --bug-report" does not pan out in that situation.

Below is the stack trace that is communicated as part of Sphinx's processing output.

[app] emitting event: 'build-finished'(StopIteration(),)
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/sphinx/cmd/build.py", line 290, in build_main
    app.build(args.force_all, args.filenames)
  File "/usr/local/lib/python3.11/site-packages/sphinx/application.py", line 351, in build
    self.builder.build_update()
  File "/usr/local/lib/python3.11/site-packages/sphinx/builders/__init__.py", line 287, in build_update
    self.build(['__all__'], to_build)
  File "/usr/local/lib/python3.11/site-packages/sphinx/builders/__init__.py", line 360, in build
    self.write(docnames, list(updated_docnames), method)
  File "/usr/local/lib/python3.11/site-packages/sphinx/builders/latex/__init__.py", line 294, in write
    doctree = self.assemble_doctree(
              ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sphinx/builders/latex/__init__.py", line 360, in assemble_doctree
    self.env.resolve_references(largetree, indexfile, self)
  File "/usr/local/lib/python3.11/site-packages/sphinx/environment/__init__.py", line 658, in resolve_references
    self.apply_post_transforms(doctree, fromdocname)
  File "/usr/local/lib/python3.11/site-packages/sphinx/environment/__init__.py", line 670, in apply_post_transforms
    transformer.apply_transforms()
  File "/usr/local/lib/python3.11/site-packages/sphinx/transforms/__init__.py", line 80, in apply_transforms
    super().apply_transforms()
  File "/usr/local/lib/python3.11/site-packages/docutils/transforms/__init__.py", line 182, in apply_transforms
    transform.apply(**kwargs)
  File "/usr/local/lib/python3.11/site-packages/sphinx/transforms/post_transforms/__init__.py", line 37, in apply
    self.run(**kwargs)
  File "/usr/local/lib/python3.11/site-packages/sphinx/builders/latex/transforms.py", line 366, in run
    self.document.walkabout(visitor)
  File "/usr/local/lib/python3.11/site-packages/docutils/nodes.py", line 186, in walkabout
    if child.walkabout(visitor):
       ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/docutils/nodes.py", line 186, in walkabout
    if child.walkabout(visitor):
       ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/docutils/nodes.py", line 186, in walkabout
    if child.walkabout(visitor):
       ^^^^^^^^^^^^^^^^^^^^^^^^
  [Previous line repeated 6 more times]
  File "/usr/local/lib/python3.11/site-packages/docutils/nodes.py", line 200, in walkabout
    visitor.dispatch_departure(self)
  File "/usr/local/lib/python3.11/site-packages/docutils/nodes.py", line 2021, in dispatch_departure
    return method(node)
           ^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/sphinx/builders/latex/transforms.py", line 441, in depart_table
    tbody = next(node.findall(nodes.tbody))
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
StopIteration
Exception occurred:
  File "/usr/local/lib/python3.11/site-packages/sphinx/builders/latex/transforms.py", line 441, in depart_table
    tbody = next(node.findall(nodes.tbody))
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
StopIteration
The full traceback has been saved in /tmp/sphinx-err-h_ja2qut.log, if you want to report the issue to the developers.
Please also report this if it was a user error, so that a better error message can be provided next time.
A bug report can be filed in the tracker at <https://github.com/sphinx-doc/sphinx/issues>. Thanks!
Sphinx extensions
On the "sphinxdoc/sphinx-latexpdf" image's Debian OS, we perform an update, an upgrade, and add packages.

  - apt-get update
  - apt-get upgrade -y
  - apt-get install -y git
  - apt-get install -y unzip
  - apt-get install -y wget
  - apt-get install -y texlive
  - apt-get install -y texlive-bibtex-extra
  - apt-get install -y texlive-font-utils
  - apt-get install -y texlive-lang-english
  - kpsewhich -var-value=TEXMFLOCAL
  - kpsewhich -var-value=TEXMFDIST
  - unzip -qo acrotex.zip
  - cd acrotex
  - |+
    for acrofile in $(ls -1 *.ins | egrep -v 'exerquiz|acrotex')
    do
      latex $acrofile
    done
    mkdir -p /usr/share/texlive/texmf-dist/tex/latex/acrotex
    cp *.sty /usr/share/texlive/texmf-dist/tex/latex/acrotex
    cp *.cfg /usr/share/texlive/texmf-dist/tex/latex/acrotex
    cp *.def /usr/share/texlive/texmf-dist/tex/latex/acrotex
  - cd ..
  - mktexlsr /usr/share/texlive/texmf-dist
  - rm -rf acrotex*

We also need to pip install a number of Python packages that are required for our documentation to be generated.

  - python -m pip install sphinx-autobuild
  - python -m pip install sphinx-git
  - python -m pip install sphinxcontrib-actdiag
  - python -m pip install sphinxcontrib-ansibleautodoc
  - python -m pip install sphinxcontrib-autoprogram
  - python -m pip install sphinxcontrib-blockdiag
  - python -m pip install sphinxcontrib-confluencebuilder
  - python -m pip install sphinxcontrib-jsonschema
  - python -m pip install sphinxcontrib-jupyter
  - python -m pip install sphinxcontrib-nwdiag
  - python -m pip install sphinxcontrib-plantuml
  - python -m pip install sphinxcontrib-seqdiag
  - python -m pip install sphinxcontrib-websupport
  - python -m pip install ciscoconfparse
  - python -m pip install decorator
  - python -m pip install enum34
  - python -m pip install funcparserlib
  - python -m pip install gitdb
  - python -m pip install Jinja2==3.0.3
  - python -m pip install jupyter-core
  - python -m pip install netaddr
  - python -m pip install plantuml
  - python -m pip install python-dateutil
  - python -m pip install pyyaml
  - python -m pip install sets
  - python -m pip install tablib
Additional context

The version of Acrotex is 2021-10-03.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in sphinx/builders/latex/transforms.py at LaTeXFootnoteVisitor.depart_table and trace the LaTeXFootnoteTransform path shown in the report. Reproduce with the provided sphinx-build -b latex command and a table without tbody nodes. Done means the LaTeX build completes without StopIteration and still generates the expected .tex output.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
documentation
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.