Skip to content

Keep smart quotes off --option mentions in help text - #104

Open
chanellebingham wants to merge 1 commit into
sphinx-doc:mainfrom
chanellebingham:fix-100-smartquotes-option-dashes
Open

chanellebingham wants to merge 1 commit into
sphinx-doc:mainfrom
chanellebingham:fix-100-smartquotes-option-dashes

Conversation

@chanellebingham

@chanellebingham chanellebingham commented Oct 22, 2026 •

Copy link
Copy Markdown

Fixes #100

With Sphinx's default smartquotes = True, an option mentioned in a description, epilog or help string renders as –option (en dash), so the documented text no longer matches what the command line accepts. The option names in the option list are not affected; only prose is.

This is based on @mmulqueen's implementation in #101, which he withdrew in July. The regular expression, the _protect_option_dashes() and _option_safe_paragraph() helpers (apart from the skip check below) and all the call sites are his: each --option mention in parser-supplied text is wrapped in a plain inline node with support_smartquotes=False, which the SmartQuotes transform skips. The commit has a Co-authored-by trailer for him.

What differs from #101

  • The :manpage: usage line. The sub-command usage line is a strong node, and smart quotes rewrote it too (run [-h] [–fast]). It is now marked support_smartquotes=False as a whole. Built with Prevent smartquotes from rewriting option dashes. #101's code, this is the only thing the new tests catch: 27 of their 29 assertions pass, and the 2 that fail are this line.
  • The skip check. Prevent smartquotes from rewriting option dashes. #101 skipped text whose direct parent is a literal or FixedTextElement. This uses sphinx.util.nodes.is_smartquotable, the check the SmartQuotes transform itself uses, which looks at every ancestor. So text deeper inside a literal block (for example emphasis in a parsed-literal) is not wrapped. The tested output is the same either way.
  • The test sample is loaded with :module: instead of :filename:, so coverage measures it (Prevent smartquotes from rewriting option dashes. #101's sample showed as 0%). Its file name is importable, so no .ruff.toml change is needed. A second test page covers :manpage: output.
  • The test fixture. cached_etree_parse now opens the built HTML with encoding='utf-8'. The new pages contain curly quotes, and the default encoding on Windows is cp1252. Linux CI is unaffected.

Tests

Two pages in the default-html test root load test/sample_smartquotes.py:

  • smartquotes.html (default output): option mentions in the description, option help, epilog and sub-command help keep both hyphens, and no –input, –output, –verbose or –quiet appears anywhere on the page. An option already written as an inline literal (--mode in double backticks) is not wrapped a second time. The epilog still reads “like these”, – like this – and east–west, and prose written in the directive body still gets its en dash.
  • smartquotes-manpage.html (:manpage:): the same for the description, epilog, option help, sub-command help and the Possible choices: line, and the usage line reads run [-h] [--fast]. In this mode help text is not parsed as reST (that was already the case), so the inline-literal check applies to the default page only.

Local results with the CI command, python -m pytest -vv --cov . --cov-append --cov-config pyproject.toml (Windows, Python 3.13, Sphinx 9.1.0, docutils 0.22.4):

  • main: 50 passed, 1 skipped.
  • This branch with sphinxarg/ext.py put back to main: 2 failed, 50 passed, 1 skipped (did not find any node matching xpath './/p/span'). The page contains <p>Read –input and write the result to –output.</p>.
  • This branch: 52 passed, 1 skipped. The page contains <p>Read <span>--input</span> and write the result to <span>--output</span>.</p>.

ruff check . --output-format github, ruff format . --diff, mypy, python -m build and twine check pass.

Sphinx 5.1.0, the minimum in pyproject.toml: the test suite doesn't run there, on main either, because the rootdir fixture returns a pathlib.Path and Sphinx 5.1's fixtures call .copytree() on it. Instead, the two test pages were built with sphinx-build under Sphinx 5.1.0 and docutils 0.19 (Python 3.12), and the test's 29 xpath assertions were checked against that HTML: 29 of 29 pass with this change, 12 of 29 on main.

Notes

  • _format_positional_arguments() gets the same change but is unreachable today: parse_parser() never sets an args key. So those lines show as uncovered.

  • The Markdown help path (:markdownhelp:) goes through the same helper. CommonMark isn't in the test extra, so CI doesn't cover it. Built by hand with CommonMark 0.9.2, main renders –input three times, –output twice, –verbose and –quiet; with this change all seven are <span>--option</span>.

  • The other test roots use :filename:. Happy to switch this one to match if you prefer; the sample would then show as uncovered.

  • No changelog entry, since the changelog looks like it's written at release time. If you'd like one in this PR, this fits the existing style:

    * `#100 <https://github.com/sphinx-doc/sphinx-argparse/issues/100>`__ Option names mentioned in help text, descriptions
      and epilogs are no longer turned into an en dash by smart quotes
    

With Sphinx's default smartquotes = True, an option mentioned in a
description, epilog or help string was rendered as "–option" (en dash),
so the documented text no longer matched what the command line accepts.

This is based on Michael Mulqueen's implementation in sphinx-doc#101, which he
withdrew in July: the same regular expression, helper functions and
call sites, wrapping each "--option" mention in an inline node with
support_smartquotes=False. Differences from sphinx-doc#101:

- The skip check uses sphinx.util.nodes.is_smartquotable, the check the
  SmartQuotes transform itself uses, instead of looking only at the
  direct parent. Text nested deeper inside a literal block (emphasis in
  a parsed-literal) is no longer wrapped. The tested output is the same.
- The sub-command usage line in :manpage: output is a strong node that
  smart quotes also rewrote ("[–fast]"); it is now marked as not
  smart-quotable as a whole.
- The test sample is loaded with :module: rather than :filename:, so
  coverage measures it. Its file name is importable, so no .ruff.toml
  change is needed.
- A second test page covers the :manpage: output.
- The etree test fixture reads the built HTML as UTF-8. The new pages
  contain curly quotes, and the default encoding on Windows is cp1252.

Fixes sphinx-doc#100

Co-authored-by: Michael Mulqueen <6616321+mmulqueen@users.noreply.github.com>
@codecov

codecov Bot commented Oct 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.53846% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.25%. Comparing base (89c7a57) to head (84b73cd).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
sphinxarg/ext.py 82.05% 6 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #104      +/-   ##
==========================================
+ Coverage   50.79%   56.25%   +5.46%     
==========================================
  Files          19       20       +1     
  Lines        1323     1367      +44     
  Branches      269      277       +8     
==========================================
+ Hits          672      769      +97     
+ Misses        605      543      -62     
- Partials       46       55       +9     
Flag Coverage Δ
unittests 56.03% <86.53%> (+5.46%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
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.

Smart quotes rewrites --option mentions in help text as an en dash

1 participant