Merge branch '4.x'

This commit is contained in:
Takeshi KOMIYA
2022-03-28 01:00:37 +09:00
12 changed files with 203 additions and 58 deletions

54
CHANGES
View File

@@ -74,7 +74,7 @@ Bugs fixed
Testing Testing
-------- --------
Release 4.5.0 (in development) Release 4.5.1 (in development)
============================== ==============================
Dependencies Dependencies
@@ -83,7 +83,27 @@ Dependencies
Incompatible changes Incompatible changes
-------------------- --------------------
Deprecated
----------
Features added
--------------
Bugs fixed
----------
Testing
--------
Release 4.5.0 (released Mar 28, 2022)
=====================================
Incompatible changes
--------------------
* #10112: extlinks: Disable hardcoded links detector by default * #10112: extlinks: Disable hardcoded links detector by default
* #9993, #10177: std domain: Disallow to refer an inline target via
:rst:role:`ref` role
Deprecated Deprecated
---------- ----------
@@ -103,17 +123,22 @@ Features added
* #9337: HTML theme, add option ``enable_search_shortcuts`` that enables :kbd:'/' as * #9337: HTML theme, add option ``enable_search_shortcuts`` that enables :kbd:'/' as
a Quick search shortcut and :kbd:`Esc` shortcut that a Quick search shortcut and :kbd:`Esc` shortcut that
removes search highlighting. removes search highlighting.
* #10107: i18n: Allow to suppress translation warnings by adding ``#noqa``
comment to the tail of each translation message
* #10252: C++, support attributes on classes, unions, and enums. * #10252: C++, support attributes on classes, unions, and enums.
* #10253: :rst:dir:`pep` role now generates URLs based on peps.python.org * #10253: :rst:dir:`pep` role now generates URLs based on peps.python.org
Bugs fixed Bugs fixed
---------- ----------
* #9876: autodoc: Failed to document an imported class that is built from native
binary module
* #10133: autodoc: Crashed when mocked module is used for type annotation * #10133: autodoc: Crashed when mocked module is used for type annotation
* #10146: autodoc: :confval:`autodoc_default_options` does not support * #10146: autodoc: :confval:`autodoc_default_options` does not support
``no-value`` option ``no-value`` option
* #9971: autodoc: TypeError is raised when the target object is annotated by * #9971: autodoc: TypeError is raised when the target object is annotated by
unhashable object unhashable object
* #10205: extlinks: Failed to compile regexp on checking hardcoded links
* #10277: html search: Could not search short words (ex. "use") * #10277: html search: Could not search short words (ex. "use")
* #9529: LaTeX: named auto numbered footnote (ex. ``[#named]``) that is referred * #9529: LaTeX: named auto numbered footnote (ex. ``[#named]``) that is referred
multiple times was rendered to a question mark multiple times was rendered to a question mark
@@ -131,33 +156,6 @@ Bugs fixed
* #10122: sphinx-build: make.bat does not check the installation of sphinx-build * #10122: sphinx-build: make.bat does not check the installation of sphinx-build
command before showing help command before showing help
Testing
--------
Release 4.4.1 (in development)
==============================
Dependencies
------------
Incompatible changes
--------------------
Deprecated
----------
Features added
--------------
Bugs fixed
----------
* #9876: autodoc: Failed to document an imported class that is built from native
binary module
Testing
--------
Release 4.4.0 (released Jan 17, 2022) Release 4.4.0 (released Jan 17, 2022)
===================================== =====================================

View File

@@ -68,6 +68,24 @@ be translated you need to follow these instructions:
* Run your desired build. * Run your desired build.
In order to protect against mistakes, a warning is emitted if
cross-references in the translated paragraph do not match those from the
original. This can be turned off globally using the
:confval:`suppress_warnings` configuration variable. Alternatively, to
turn it off for one message only, end the message with ``#noqa`` like
this::
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
risus tortor, luctus id ultrices at. #noqa
(Write ``\#noqa`` in case you want to have "#noqa" literally in the
text. This does not apply to code blocks, where ``#noqa`` is ignored
because code blocks do not contain references anyway.)
.. versionadded:: 4.5
The ``#noqa`` mechanism.
Translating with sphinx-intl Translating with sphinx-intl
---------------------------- ----------------------------

View File

@@ -316,7 +316,11 @@ General configuration
* ``app.add_role`` * ``app.add_role``
* ``app.add_generic_role`` * ``app.add_generic_role``
* ``app.add_source_parser`` * ``app.add_source_parser``
* ``autosectionlabel.*``
* ``download.not_readable`` * ``download.not_readable``
* ``epub.unknown_project_files``
* ``epub.duplicated_toc_entry``
* ``i18n.inconsistent_references``
* ``image.not_readable`` * ``image.not_readable``
* ``ref.term`` * ``ref.term``
* ``ref.ref`` * ``ref.ref``
@@ -332,11 +336,9 @@ General configuration
* ``toc.excluded`` * ``toc.excluded``
* ``toc.not_readable`` * ``toc.not_readable``
* ``toc.secnum`` * ``toc.secnum``
* ``epub.unknown_project_files``
* ``epub.duplicated_toc_entry``
* ``autosectionlabel.*``
You can choose from these types. You can choose from these types. You can also give only the first
component to exclude all warnings attached to it.
Now, this option should be considered *experimental*. Now, this option should be considered *experimental*.
@@ -366,6 +368,10 @@ General configuration
Added ``toc.excluded`` and ``toc.not_readable`` Added ``toc.excluded`` and ``toc.not_readable``
.. versionadded:: 4.5
Added ``i18n.inconsistent_references``
.. confval:: needs_sphinx .. confval:: needs_sphinx
If set to a ``major.minor`` version string like ``'1.1'``, Sphinx will If set to a ``major.minor`` version string like ``'1.1'``, Sphinx will

View File

@@ -747,11 +747,10 @@ class StandardDomain(Domain):
sectname = clean_astext(title) sectname = clean_astext(title)
elif node.tagname == 'rubric': elif node.tagname == 'rubric':
sectname = clean_astext(node) sectname = clean_astext(node)
elif node.tagname == 'target' and len(node) > 0:
# inline target (ex: blah _`blah` blah)
sectname = clean_astext(node)
elif self.is_enumerable_node(node): elif self.is_enumerable_node(node):
sectname = self.get_numfig_title(node) sectname = self.get_numfig_title(node)
if not sectname:
continue
else: else:
toctree = next(node.findall(addnodes.toctree), None) toctree = next(node.findall(addnodes.toctree), None)
if toctree and toctree.get('caption'): if toctree and toctree.get('caption'):
@@ -759,8 +758,7 @@ class StandardDomain(Domain):
else: else:
# anonymous-only labels # anonymous-only labels
continue continue
if sectname: self.labels[name] = docname, labelid, sectname
self.labels[name] = docname, labelid, sectname
def add_program_option(self, program: str, name: str, docname: str, labelid: str) -> None: def add_program_option(self, program: str, name: str, docname: str, labelid: str) -> None:
self.progoptions[program, name] = (docname, labelid) self.progoptions[program, name] = (docname, labelid)

View File

@@ -18,6 +18,7 @@ Both, the url string and the caption string must escape ``%`` as ``%%``.
""" """
import re import re
import sys
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
from docutils import nodes, utils from docutils import nodes, utils
@@ -63,7 +64,13 @@ class ExternalLinksChecker(SphinxPostTransform):
title = refnode.astext() title = refnode.astext()
for alias, (base_uri, _caption) in self.app.config.extlinks.items(): for alias, (base_uri, _caption) in self.app.config.extlinks.items():
uri_pattern = re.compile(base_uri.replace('%s', '(?P<value>.+)')) if sys.version_info < (3, 7):
# Replace a leading backslash because re.escape() inserts a backslash before %
# on python 3.6
uri_pattern = re.compile(re.escape(base_uri).replace('\\%s', '(?P<value>.+)'))
else:
uri_pattern = re.compile(re.escape(base_uri).replace('%s', '(?P<value>.+)'))
match = uri_pattern.match(uri) match = uri_pattern.match(uri)
if match and match.groupdict().get('value'): if match and match.groupdict().get('value'):
# build a replacement suggestion # build a replacement suggestion

View File

@@ -1,6 +1,7 @@
"""Docutils transforms used by Sphinx when reading documents.""" """Docutils transforms used by Sphinx when reading documents."""
from os import path from os import path
from re import DOTALL, match
from textwrap import indent from textwrap import indent
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, TypeVar from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, TypeVar
@@ -74,6 +75,14 @@ def publish_msgstr(app: "Sphinx", source: str, source_path: str, source_line: in
config.rst_prolog = rst_prolog # type: ignore config.rst_prolog = rst_prolog # type: ignore
def parse_noqa(source: str) -> Tuple[str, bool]:
m = match(r"(.*)(?<!\\)#\s*noqa\s*$", source, DOTALL)
if m:
return m.group(1), True
else:
return source, False
class PreserveTranslatableMessages(SphinxTransform): class PreserveTranslatableMessages(SphinxTransform):
""" """
Preserve original translatable messages before translation Preserve original translatable messages before translation
@@ -111,6 +120,14 @@ class Locale(SphinxTransform):
# phase1: replace reference ids with translated names # phase1: replace reference ids with translated names
for node, msg in extract_messages(self.document): for node, msg in extract_messages(self.document):
msgstr = catalog.gettext(msg) msgstr = catalog.gettext(msg)
# There is no point in having #noqa on literal blocks because
# they cannot contain references. Recognizing it would just
# completely prevent escaping the #noqa. Outside of literal
# blocks, one can always write \#noqa.
if not isinstance(node, LITERAL_TYPE_NODES):
msgstr, _ = parse_noqa(msgstr)
# XXX add marker to untranslated parts # XXX add marker to untranslated parts
if not msgstr or msgstr == msg or not msgstr.strip(): if not msgstr or msgstr == msg or not msgstr.strip():
# as-of-yet untranslated # as-of-yet untranslated
@@ -131,6 +148,7 @@ class Locale(SphinxTransform):
patch = publish_msgstr(self.app, msgstr, source, patch = publish_msgstr(self.app, msgstr, source,
node.line, self.config, settings) node.line, self.config, settings)
# FIXME: no warnings about inconsistent references in this part
# XXX doctest and other block markup # XXX doctest and other block markup
if not isinstance(patch, nodes.paragraph): if not isinstance(patch, nodes.paragraph):
continue # skip for now continue # skip for now
@@ -220,6 +238,11 @@ class Locale(SphinxTransform):
continue # skip if the node is already translated by phase1 continue # skip if the node is already translated by phase1
msgstr = catalog.gettext(msg) msgstr = catalog.gettext(msg)
# See above.
if not isinstance(node, LITERAL_TYPE_NODES):
msgstr, noqa = parse_noqa(msgstr)
# XXX add marker to untranslated parts # XXX add marker to untranslated parts
if not msgstr or msgstr == msg: # as-of-yet untranslated if not msgstr or msgstr == msg: # as-of-yet untranslated
continue continue
@@ -265,7 +288,6 @@ class Locale(SphinxTransform):
patch = publish_msgstr(self.app, msgstr, source, patch = publish_msgstr(self.app, msgstr, source,
node.line, self.config, settings) node.line, self.config, settings)
# Structural Subelements phase2 # Structural Subelements phase2
if isinstance(node, nodes.title): if isinstance(node, nodes.title):
# get <title> node that placed as a first child # get <title> node that placed as a first child
@@ -295,13 +317,13 @@ class Locale(SphinxTransform):
is_autofootnote_ref = NodeMatcher(nodes.footnote_reference, auto=Any) is_autofootnote_ref = NodeMatcher(nodes.footnote_reference, auto=Any)
old_foot_refs: List[nodes.footnote_reference] = list(node.findall(is_autofootnote_ref)) # NOQA old_foot_refs: List[nodes.footnote_reference] = list(node.findall(is_autofootnote_ref)) # NOQA
new_foot_refs: List[nodes.footnote_reference] = list(patch.findall(is_autofootnote_ref)) # NOQA new_foot_refs: List[nodes.footnote_reference] = list(patch.findall(is_autofootnote_ref)) # NOQA
if len(old_foot_refs) != len(new_foot_refs): if not noqa and len(old_foot_refs) != len(new_foot_refs):
old_foot_ref_rawsources = [ref.rawsource for ref in old_foot_refs] old_foot_ref_rawsources = [ref.rawsource for ref in old_foot_refs]
new_foot_ref_rawsources = [ref.rawsource for ref in new_foot_refs] new_foot_ref_rawsources = [ref.rawsource for ref in new_foot_refs]
logger.warning(__('inconsistent footnote references in translated message.' + logger.warning(__('inconsistent footnote references in translated message.' +
' original: {0}, translated: {1}') ' original: {0}, translated: {1}')
.format(old_foot_ref_rawsources, new_foot_ref_rawsources), .format(old_foot_ref_rawsources, new_foot_ref_rawsources),
location=node) location=node, type='i18n', subtype='inconsistent_references')
old_foot_namerefs: Dict[str, List[nodes.footnote_reference]] = {} old_foot_namerefs: Dict[str, List[nodes.footnote_reference]] = {}
for r in old_foot_refs: for r in old_foot_refs:
old_foot_namerefs.setdefault(r.get('refname'), []).append(r) old_foot_namerefs.setdefault(r.get('refname'), []).append(r)
@@ -338,13 +360,13 @@ class Locale(SphinxTransform):
is_refnamed_ref = NodeMatcher(nodes.reference, refname=Any) is_refnamed_ref = NodeMatcher(nodes.reference, refname=Any)
old_refs: List[nodes.reference] = list(node.findall(is_refnamed_ref)) old_refs: List[nodes.reference] = list(node.findall(is_refnamed_ref))
new_refs: List[nodes.reference] = list(patch.findall(is_refnamed_ref)) new_refs: List[nodes.reference] = list(patch.findall(is_refnamed_ref))
if len(old_refs) != len(new_refs): if not noqa and len(old_refs) != len(new_refs):
old_ref_rawsources = [ref.rawsource for ref in old_refs] old_ref_rawsources = [ref.rawsource for ref in old_refs]
new_ref_rawsources = [ref.rawsource for ref in new_refs] new_ref_rawsources = [ref.rawsource for ref in new_refs]
logger.warning(__('inconsistent references in translated message.' + logger.warning(__('inconsistent references in translated message.' +
' original: {0}, translated: {1}') ' original: {0}, translated: {1}')
.format(old_ref_rawsources, new_ref_rawsources), .format(old_ref_rawsources, new_ref_rawsources),
location=node) location=node, type='i18n', subtype='inconsistent_references')
old_ref_names = [r['refname'] for r in old_refs] old_ref_names = [r['refname'] for r in old_refs]
new_ref_names = [r['refname'] for r in new_refs] new_ref_names = [r['refname'] for r in new_refs]
orphans = list(set(old_ref_names) - set(new_ref_names)) orphans = list(set(old_ref_names) - set(new_ref_names))
@@ -366,13 +388,13 @@ class Locale(SphinxTransform):
old_foot_refs = list(node.findall(is_refnamed_footnote_ref)) old_foot_refs = list(node.findall(is_refnamed_footnote_ref))
new_foot_refs = list(patch.findall(is_refnamed_footnote_ref)) new_foot_refs = list(patch.findall(is_refnamed_footnote_ref))
refname_ids_map: Dict[str, List[str]] = {} refname_ids_map: Dict[str, List[str]] = {}
if len(old_foot_refs) != len(new_foot_refs): if not noqa and len(old_foot_refs) != len(new_foot_refs):
old_foot_ref_rawsources = [ref.rawsource for ref in old_foot_refs] old_foot_ref_rawsources = [ref.rawsource for ref in old_foot_refs]
new_foot_ref_rawsources = [ref.rawsource for ref in new_foot_refs] new_foot_ref_rawsources = [ref.rawsource for ref in new_foot_refs]
logger.warning(__('inconsistent footnote references in translated message.' + logger.warning(__('inconsistent footnote references in translated message.' +
' original: {0}, translated: {1}') ' original: {0}, translated: {1}')
.format(old_foot_ref_rawsources, new_foot_ref_rawsources), .format(old_foot_ref_rawsources, new_foot_ref_rawsources),
location=node) location=node, type='i18n', subtype='inconsistent_references')
for oldf in old_foot_refs: for oldf in old_foot_refs:
refname_ids_map.setdefault(oldf["refname"], []).append(oldf["ids"]) refname_ids_map.setdefault(oldf["refname"], []).append(oldf["ids"])
for newf in new_foot_refs: for newf in new_foot_refs:
@@ -385,13 +407,13 @@ class Locale(SphinxTransform):
old_cite_refs: List[nodes.citation_reference] = list(node.findall(is_citation_ref)) old_cite_refs: List[nodes.citation_reference] = list(node.findall(is_citation_ref))
new_cite_refs: List[nodes.citation_reference] = list(patch.findall(is_citation_ref)) # NOQA new_cite_refs: List[nodes.citation_reference] = list(patch.findall(is_citation_ref)) # NOQA
refname_ids_map = {} refname_ids_map = {}
if len(old_cite_refs) != len(new_cite_refs): if not noqa and len(old_cite_refs) != len(new_cite_refs):
old_cite_ref_rawsources = [ref.rawsource for ref in old_cite_refs] old_cite_ref_rawsources = [ref.rawsource for ref in old_cite_refs]
new_cite_ref_rawsources = [ref.rawsource for ref in new_cite_refs] new_cite_ref_rawsources = [ref.rawsource for ref in new_cite_refs]
logger.warning(__('inconsistent citation references in translated message.' + logger.warning(__('inconsistent citation references in translated message.' +
' original: {0}, translated: {1}') ' original: {0}, translated: {1}')
.format(old_cite_ref_rawsources, new_cite_ref_rawsources), .format(old_cite_ref_rawsources, new_cite_ref_rawsources),
location=node) location=node, type='i18n', subtype='inconsistent_references')
for oldc in old_cite_refs: for oldc in old_cite_refs:
refname_ids_map.setdefault(oldc["refname"], []).append(oldc["ids"]) refname_ids_map.setdefault(oldc["refname"], []).append(oldc["ids"])
for newc in new_cite_refs: for newc in new_cite_refs:
@@ -405,13 +427,13 @@ class Locale(SphinxTransform):
old_xrefs = list(node.findall(addnodes.pending_xref)) old_xrefs = list(node.findall(addnodes.pending_xref))
new_xrefs = list(patch.findall(addnodes.pending_xref)) new_xrefs = list(patch.findall(addnodes.pending_xref))
xref_reftarget_map = {} xref_reftarget_map = {}
if len(old_xrefs) != len(new_xrefs): if not noqa and len(old_xrefs) != len(new_xrefs):
old_xref_rawsources = [xref.rawsource for xref in old_xrefs] old_xref_rawsources = [xref.rawsource for xref in old_xrefs]
new_xref_rawsources = [xref.rawsource for xref in new_xrefs] new_xref_rawsources = [xref.rawsource for xref in new_xrefs]
logger.warning(__('inconsistent term references in translated message.' + logger.warning(__('inconsistent term references in translated message.' +
' original: {0}, translated: {1}') ' original: {0}, translated: {1}')
.format(old_xref_rawsources, new_xref_rawsources), .format(old_xref_rawsources, new_xref_rawsources),
location=node) location=node, type='i18n', subtype='inconsistent_references')
def get_ref_key(node: addnodes.pending_xref) -> Optional[Tuple[str, str, str]]: def get_ref_key(node: addnodes.pending_xref) -> Optional[Tuple[str, str, str]]:
case = node["refdomain"], node["reftype"] case = node["refdomain"], node["reftype"]

View File

@@ -49,6 +49,14 @@ code blocks
literal-block literal-block
in list in list
.. highlight:: none
::
test_code_for_noqa()
continued()
doctest blocks doctest blocks
============== ==============

View File

@@ -0,0 +1,16 @@
First section
=============
Some text with a reference, :ref:`next-section`.
Another reference: :ref:`next-section`.
This should allow to test escaping ``#noqa``.
.. _next-section:
Next section
============
Some text, again referring to the section: :ref:`next-section`.

View File

@@ -77,6 +77,12 @@ msgid "literal-block\n"
msgstr "LITERAL-BLOCK\n" msgstr "LITERAL-BLOCK\n"
"IN LIST" "IN LIST"
msgid "test_code_for_noqa()\n"
"continued()"
msgstr ""
"# TRAILING noqa SHOULD NOT GET STRIPPED\n"
"# FROM THIS BLOCK. #noqa"
msgid "doctest blocks" msgid "doctest blocks"
msgstr "DOCTEST-BLOCKS" msgstr "DOCTEST-BLOCKS"

View File

@@ -0,0 +1,46 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C)
# This file is distributed under the same license as the Sphinx intl <Tests> package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-01-16 15:23+0100\n"
"PO-Revision-Date: 2022-01-16 15:23+0100\n"
"Last-Translator: Jean Abou Samra <jean@abou-samra.fr>\n"
"Language-Team: \n"
"Language: xx\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0\n"
#: ../tests/roots/test-intl/noqa.txt:2
msgid "First section"
msgstr "FIRST SECTION"
#: ../tests/roots/test-intl/noqa.txt:4
msgid "Some text with a reference, :ref:`next-section`."
msgstr "TRANSLATED TEXT WITHOUT REFERENCE. #noqa"
#: ../tests/roots/test-intl/noqa.txt:6
msgid "Another reference: :ref:`next-section`."
msgstr ""
"TEST noqa WHITESPACE INSENSITIVITY.\n"
"# \n"
" noqa"
#: ../tests/roots/test-intl/noqa.txt:8
msgid "This should allow to test escaping ``#noqa``."
msgstr "``#noqa`` IS ESCAPED AT THE END OF THIS STRING. \\#noqa"
#: ../tests/roots/test-intl/noqa.txt:13
msgid "Next section"
msgstr "NEXT SECTION WITH PARAGRAPH TO TEST BARE noqa"
# This edge case should not fail.
#: ../tests/roots/test-intl/noqa.txt:15
msgid "Some text, again referring to the section: :ref:`next-section`."
msgstr "#noqa"

View File

@@ -445,12 +445,3 @@ def test_labeled_rubric(app):
domain = app.env.get_domain("std") domain = app.env.get_domain("std")
assert 'label' in domain.labels assert 'label' in domain.labels
assert domain.labels['label'] == ('index', 'label', 'blah blah blah') assert domain.labels['label'] == ('index', 'label', 'blah blah blah')
def test_inline_target(app):
text = "blah _`inline target` blah\n"
restructuredtext.parse(app, text)
domain = app.env.get_domain("std")
assert 'inline target' in domain.labels
assert domain.labels['inline target'] == ('index', 'inline-target', 'inline target')

View File

@@ -186,6 +186,32 @@ def test_text_inconsistency_warnings(app, warning):
assert_re_search(expected_citation_warning_expr, warnings) assert_re_search(expected_citation_warning_expr, warnings)
@sphinx_intl
@pytest.mark.sphinx('text')
@pytest.mark.test_params(shared_result='test_intl_basic')
def test_noqa(app, warning):
app.build()
result = (app.outdir / 'noqa.txt').read_text()
expect = r"""FIRST SECTION
*************
TRANSLATED TEXT WITHOUT REFERENCE.
TEST noqa WHITESPACE INSENSITIVITY.
"#noqa" IS ESCAPED AT THE END OF THIS STRING. #noqa
NEXT SECTION WITH PARAGRAPH TO TEST BARE noqa
*********************************************
Some text, again referring to the section: NEXT SECTION WITH PARAGRAPH
TO TEST BARE noqa.
"""
assert result == expect
assert "next-section" not in getwarning(warning)
@sphinx_intl @sphinx_intl
@pytest.mark.sphinx('text') @pytest.mark.sphinx('text')
@pytest.mark.test_params(shared_result='test_intl_basic') @pytest.mark.test_params(shared_result='test_intl_basic')
@@ -1180,6 +1206,9 @@ def test_additional_targets_should_be_translated(app):
"""<span class="c1"># SYS IMPORTING</span>""") """<span class="c1"># SYS IMPORTING</span>""")
assert_count(expected_expr, result, 1) assert_count(expected_expr, result, 1)
# '#noqa' should remain in literal blocks.
assert_count("#noqa", result, 1)
# [raw.txt] # [raw.txt]
result = (app.outdir / 'raw.html').read_text() result = (app.outdir / 'raw.html').read_text()