Skip to content

Skip unknown RST directives in introspected docstrings - #134

Open
DSeaStar wants to merge 1 commit into
anntzer:mainfrom
DSeaStar:skip-unknown-rst-directives
Open

DSeaStar wants to merge 1 commit into
anntzer:mainfrom
DSeaStar:skip-unknown-rst-directives

Conversation

@DSeaStar

Copy link
Copy Markdown

Summary

Fixes #133.

defopt.run aborted with a docutils.utils.SystemMessage (ERROR/3) when an introspected docstring contained a Sphinx-only directive such as .. versionchanged::. That happens both for the command function itself and for third-party types whose constructor docstring is parsed (the original report used pathlib.Path).

_parse_docstring already registers a few Sphinx roles, but unknown directives still produced a fatal error because halt_level=3. This PR treats unknown directives as skippable (consume the block, emit no error) so parsing continues. Completely invalid RST still raises via the existing halt level.

Test plan

  • test_sphinx_directive_in_docstring — parse a function docstring that contains .. versionchanged:: and still extract the description + :param: docs
  • test_sphinx_directive_does_not_abort_run — defopt.run succeeds for that function
  • test_sphinx_directive_in_type_docstring — a custom type whose class docstring contains a Sphinx directive no longer aborts defopt.run
  • test_bad_doc still raises SystemMessage on genuinely invalid RST
  • full suite: 170 passed

Sphinx-only directives such as versionchanged were treated as fatal
ERROR-level messages, so defopt.run aborted when a function or a
third-party type docstring contained them. Completely invalid RST still
raises.
Comment thread src/defopt.py
# Consume the directive block the same way docutils does, then
# continue parsing instead of reporting an ERROR.
_indented, _indent, _offset, blank_finish = (
self.state_machine.get_first_known_indented(0, strip_indent=False))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain (briefly) the choice of using get_first_known_indented here rather than e.g. get_indented or get_known_indented?

@DSeaStar

Copy link
Copy Markdown
Author

Can you explain (briefly) the choice of using get_first_known_indented here rather than e.g. get_indented or get_known_indented?

get_first_known_indented is the same call that docutils' own Body.unknown_directive makes internally (in docutils.parsers.rst.states.RSTState.unknown_directive) to consume the directive block before raising the error. Using it here replicates that exact block-consumption logic — the directive body (arguments, options, content) is fully read and discarded so the state machine can continue parsing the rest of the document.

get_indented wouldn't work because it expects a known indent level and is meant for line-by-line iteration within a directive's content parsing, not for consuming an unknown directive's entire block upfront. get_known_indented has a similar mismatch — it's designed for contexts where the indent is already determined.

TL;DR: it's the standard docutils way to "eat" an unknown directive so parsing can continue.

Comment thread test_defopt.py
with self.assertRaises(SystemMessage):
defopt._parse_docstring(inspect.cleandoc(doc))

def test_sphinx_directive_in_docstring(self):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't need to be three separate tests (they are effectively all exercising the same codepath). OTOH the test needs to check that the directive is indeed completely stripped and does not appear in the help-text.

@anntzer

anntzer commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Is the patch AI-generated? This is fine, but needs to be properly disclosed (in particular in the commit message).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

defopt.run aborts with a docutils SystemMessage when an introspected docstring contains a Sphinx directive (e.g. .. versionchanged::)

2 participants