From 9372cf82c4690d888c68f677818ed951caced90b Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 15 Feb 2021 04:13:07 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20NEW:=20Add=20warning=20types=20`mys?= =?UTF-8?q?t.subtype`=20(#313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All parsing warnings are assigned a type/subtype, and also the messages are appended with them. These warning types can be suppressed with the sphinx `suppress_warnings` config option. --- docs/using/howto.md | 22 ++++ myst_parser/docutils_renderer.py | 122 +++++++++++------- myst_parser/html_to_nodes.py | 9 +- myst_parser/myst_refs.py | 14 +- myst_parser/sphinx_renderer.py | 40 ++++-- tests/test_renderers/fixtures/containers.md | 4 +- .../fixtures/sphinx_directives.md | 4 +- tests/test_renderers/test_fixtures.py | 9 +- 8 files changed, 153 insertions(+), 71 deletions(-) diff --git a/docs/using/howto.md b/docs/using/howto.md index a3c40aa6..56b6ee5e 100644 --- a/docs/using/howto.md +++ b/docs/using/howto.md @@ -169,3 +169,25 @@ like so: ```md {ref}`path/to/file_1:My Subtitle` ``` + +(howto/warnings)= +## Suppress warnings + +In general, if your build logs any warnings, you should either fix them or [raise an Issue](https://github.com/executablebooks/MyST-Parser/issues/new/choose) if you think the warning is erroneous. +However, in some circumstances if you wish to suppress the warning you can use the [`suppress_warnings`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-suppress_warnings) configuration option. +All myst-parser warnings are prepended by their type, e.g. to suppress: + +```md +# Title +### Subtitle +``` + +``` +WARNING: Non-consecutive header level increase; 1 to 3 [myst.header] +``` + +Add to your `conf.py`: + +```python +suppress_warnings = ["myst.header"] +``` diff --git a/myst_parser/docutils_renderer.py b/myst_parser/docutils_renderer.py index 666f336d..a69f1d79 100644 --- a/myst_parser/docutils_renderer.py +++ b/myst_parser/docutils_renderer.py @@ -114,6 +114,26 @@ def setup_render(self, options: Dict[str, Any], env: Dict[str, Any]): ) self._level_to_elem: Dict[int, nodes.Element] = {0: self.document} + def create_warning( + self, + message: str, + *, + line: Optional[int] = None, + append_to: Optional[nodes.Element] = None, + wtype: str = "myst", + subtype: str = "other", + ) -> Optional[nodes.system_message]: + """Generate a warning, logging if it is necessary. + + Note this is overridden in the ``SphinxRenderer``, + to handle suppressed warning types. + """ + kwargs = {"line": line} if line is not None else {} + msg_node = self.reporter.warning(message, **kwargs) + if append_to is not None: + append_to.append(msg_node) + return msg_node + def render(self, tokens: List[Token], options, env: AttrDict): """Run the render on a token stream. @@ -153,21 +173,21 @@ def render(self, tokens: List[Token], options, env: AttrDict): if f"render_{nest_token.type}" in self.rules: self.rules[f"render_{nest_token.type}"](nest_token) else: - self.current_node.append( - self.reporter.warning( - f"No render method for: {nest_token.type}", - line=token_line(nest_token, default=0), - ) + self.create_warning( + f"No render method for: {nest_token.type}", + line=token_line(nest_token, default=0), + subtype="render", + append_to=self.current_node, ) # log warnings for duplicate reference definitions # "duplicate_refs": [{"href": "ijk", "label": "B", "map": [4, 5], "title": ""}], for dup_ref in self.env.get("duplicate_refs", []): - self.document.append( - self.reporter.warning( - f"Duplicate reference definition: {dup_ref['label']}", - line=dup_ref["map"][0] + 1, - ) + self.create_warning( + f"Duplicate reference definition: {dup_ref['label']}", + line=dup_ref["map"][0] + 1, + subtype="ref", + append_to=self.document, ) if not self.config.get("output_footnotes", True): @@ -187,16 +207,17 @@ def render(self, tokens: List[Token], options, env: AttrDict): for footref in foot_refs: foot_ref_tokens = self.env["foot_refs"].get(footref, []) if len(foot_ref_tokens) > 1: - self.current_node.append( - self.reporter.warning( - f"Multiple footnote definitions found for label: '{footref}'", - ) + self.create_warning( + f"Multiple footnote definitions found for label: '{footref}'", + subtype="footnote", + append_to=self.current_node, ) + if len(foot_ref_tokens) < 1: - self.current_node.append( - self.reporter.warning( - f"No footnote definitions found for label: '{footref}'", - ) + self.create_warning( + f"No footnote definitions found for label: '{footref}'", + subtype="footnote", + append_to=self.current_node, ) else: self.render_footnote_reference_open(foot_ref_tokens[0]) @@ -237,11 +258,11 @@ def nested_render_text(self, text: str, lineno: int): if f"render_{nest_token.type}" in self.rules: self.rules[f"render_{nest_token.type}"](nest_token) else: - self.current_node.append( - self.reporter.warning( - f"No render method for: {nest_token.type}", - line=token_line(nest_token, default=0), - ) + self.create_warning( + f"No render method for: {nest_token.type}", + line=token_line(nest_token, default=0), + subtype="render", + append_to=self.current_node, ) @contextmanager @@ -259,11 +280,11 @@ def render_children(self, token: Union[Token, NestedTokens]): if f"render_{child.type}" in self.rules: self.rules[f"render_{child.type}"](child) else: - self.current_node.append( - self.reporter.warning( - f"No render method for: {child.type}", - line=token_line(child, default=0), - ) + self.create_warning( + f"No render method for: {child.type}", + line=token_line(child, default=0), + subtype="render", + append_to=self.current_node, ) def add_line_and_source_path(self, node, token: Union[Token, NestedTokens]): @@ -285,13 +306,13 @@ def add_section(self, section, level): ) if (level > parent_level) and (parent_level + 1 != level): - self.current_node.append( - self.reporter.warning( - "Non-consecutive header level increase; {} to {}".format( - parent_level, level - ), - line=section.line, - ) + self.create_warning( + "Non-consecutive header level increase; {} to {}".format( + parent_level, level + ), + line=section.line, + subtype="header", + append_to=self.current_node, ) parent = self._level_to_elem[parent_level] @@ -511,10 +532,11 @@ def render_link_open(self, token: NestedTokens): def handle_cross_reference(self, token, destination): if not self.config.get("ignore_missing_refs", False): - self.current_node.append( - self.reporter.warning( - f"Reference not found: {destination}", line=token_line(token) - ) + self.create_warning( + f"Reference not found: {destination}", + line=token_line(token), + subtype="ref", + append_to=self.current_node, ) def render_autolink(self, token: NestedTokens): @@ -829,21 +851,21 @@ def render_colon_fence(self, token: Token): classes = match.groupdict()["classes"][1:].split(",") name = match.groupdict()["name"] if classes and classes[0]: - self.current_node.append( - self.reporter.warning( - "comma-separated classes are deprecated, " - "use `:class:` option instead", - line=token_line(token), - ) + self.create_warning( + "comma-separated classes are deprecated, " + "use `:class:` option instead", + line=token_line(token), + subtype="deprecation", + append_to=self.current_node, ) # we assume that no other options have been used token.content = f":class: {' '.join(classes)}\n\n" + token.content if name == "figure": - self.current_node.append( - self.reporter.warning( - ":::{figure} is deprecated, " "use :::{figure-md} instead", - line=token_line(token), - ) + self.create_warning( + ":::{figure} is deprecated, " "use :::{figure-md} instead", + line=token_line(token), + subtype="deprecation", + append_to=self.current_node, ) name = "figure-md" diff --git a/myst_parser/html_to_nodes.py b/myst_parser/html_to_nodes.py index 45ceb6bc..1576a216 100644 --- a/myst_parser/html_to_nodes.py +++ b/myst_parser/html_to_nodes.py @@ -47,9 +47,12 @@ def html_to_nodes( try: root = tokenize_html(text).strip(inplace=True, recurse=False) except Exception: - return [ - renderer.reporter.warning("HTML could not be parsed", line=line_number) - ] + default_html(text, renderer.document["source"], line_number) + msg_node = renderer.create_warning( + "HTML could not be parsed", line=line_number, subtype="html" + ) + return ([msg_node] if msg_node else []) + default_html( + text, renderer.document["source"], line_number + ) if len(root) < 1: # if empty diff --git a/myst_parser/myst_refs.py b/myst_parser/myst_refs.py index 8724c491..4e01db27 100644 --- a/myst_parser/myst_refs.py +++ b/myst_parser/myst_refs.py @@ -140,11 +140,19 @@ def resolve_myst_ref( except NotImplementedError: # the domain doesn't yet support the new interface # we have to manually collect possible references (SLOW) + if not getattr(domain, "__module__", "").startswith("sphinx."): + logger.warning( + f"Domain '{domain.__module__}::{domain.name}' has not " + "implemented a `resolve_any_xref` method [myst.domains]", + type="myst", + subtype="domains", + once=True, + ) for role in domain.roles: res = domain.resolve_xref( self.env, refdoc, self.app.builder, role, target, node, contnode ) - if res and isinstance(res[0], nodes.Element): + if len(res) and isinstance(res[0], nodes.Element): results.append((f"{domain.name}:{role}", res)) # now, see how many matches we got... @@ -160,9 +168,11 @@ def stringify(name, node): logger.warning( __( f"more than one target found for 'myst' cross-reference {target}: " - f"could be {candidates}" + f"could be {candidates} [myst.ref]" ), location=node, + type="myst", + subtype="ref", ) res_role, newnode = results[0] diff --git a/myst_parser/sphinx_renderer.py b/myst_parser/sphinx_renderer.py index 94075b12..4ceaa22f 100644 --- a/myst_parser/sphinx_renderer.py +++ b/myst_parser/sphinx_renderer.py @@ -2,7 +2,7 @@ import tempfile from contextlib import contextmanager from io import StringIO -from typing import cast +from typing import Optional, cast from urllib.parse import unquote from uuid import uuid4 @@ -16,7 +16,6 @@ from sphinx.domains.std import StandardDomain from sphinx.environment import BuildEnvironment from sphinx.events import EventManager -from sphinx.locale import __ from sphinx.project import Project from sphinx.registry import SphinxComponentRegistry from sphinx.util import logging @@ -40,6 +39,31 @@ class SphinxRenderer(DocutilsRenderer): def doc_env(self) -> BuildEnvironment: return self.document.settings.env + def create_warning( + self, + message: str, + *, + line: Optional[int] = None, + append_to: Optional[nodes.Element] = None, + wtype: str = "myst", + subtype: str = "other", + ) -> Optional[nodes.system_message]: + """Generate a warning, logging it if necessary. + + If the warning type is listed in the ``suppress_warnings`` configuration, + then ``None`` will be returned and no warning logged. + """ + message = f"{message} [{wtype}.{subtype}]" + kwargs = {"line": line} if line is not None else {} + + if not logging.is_suppressed_warning( + wtype, subtype, self.doc_env.app.config.suppress_warnings + ): + msg_node = self.reporter.warning(message, **kwargs) + if append_to is not None: + append_to.append(msg_node) + return None + def handle_cross_reference(self, token: Token, destination: str): """Create nodes for references that are not immediately resolvable.""" wrap_node = addnodes.pending_xref( @@ -79,13 +103,11 @@ def render_heading_open(self, token: NestedTokens): # save the reference in the standard domain, so that it can be handled properly domain = cast(StandardDomain, self.doc_env.get_domain("std")) if doc_slug in domain.labels: - LOGGER.warning( - __("duplicate label %s, other instance in %s"), - doc_slug, - self.doc_env.doc2path(domain.labels[doc_slug][0]), - location=section, - type="myst-anchor", - subtype=self.doc_env.docname, + other_doc = self.doc_env.doc2path(domain.labels[doc_slug][0]) + self.create_warning( + f"duplicate label {doc_slug}, other instance in {other_doc}", + line=section.line, + subtype="anchor", ) labelid = section["ids"][0] domain.anonlabels[doc_slug] = self.doc_env.docname, labelid diff --git a/tests/test_renderers/fixtures/containers.md b/tests/test_renderers/fixtures/containers.md index 415ae482..f2fd2da1 100644 --- a/tests/test_renderers/fixtures/containers.md +++ b/tests/test_renderers/fixtures/containers.md @@ -25,7 +25,7 @@ Nested notes: - comma-separated classes are deprecated, use `:class:` option instead + comma-separated classes are deprecated, use `:class:` option instead [myst.deprecation] @@ -42,7 +42,7 @@ Admonition with title: - comma-separated classes are deprecated, use `:class:` option instead + comma-separated classes are deprecated, use `:class:` option instead [myst.deprecation] A diff --git a/tests/test_renderers/fixtures/sphinx_directives.md b/tests/test_renderers/fixtures/sphinx_directives.md index 3206370b..906faf72 100644 --- a/tests/test_renderers/fixtures/sphinx_directives.md +++ b/tests/test_renderers/fixtures/sphinx_directives.md @@ -178,7 +178,7 @@ acks (`sphinx.directives.other.Acks`): . -------------------------------- -hlist (`sphinx.directives.other.HList`): +SPHINX3.5 hlist (`sphinx.directives.other.HList`): . ```{hlist} @@ -186,7 +186,7 @@ hlist (`sphinx.directives.other.HList`): ``` . <document source="notset"> - <hlist> + <hlist ncolumns="2"> <hlistcol> <bullet_list> <list_item> diff --git a/tests/test_renderers/test_fixtures.py b/tests/test_renderers/test_fixtures.py index 9d95649e..3850d254 100644 --- a/tests/test_renderers/test_fixtures.py +++ b/tests/test_renderers/test_fixtures.py @@ -108,15 +108,18 @@ def test_sphinx_directives(line, title, input, expected): # TODO test domain directives if title.startswith("SKIP"): pytest.skip(title) - if title.startswith("SPHINX3") and sphinx.version_info[0] < 3: + elif title.startswith("SPHINX3") and sphinx.version_info[0] < 3: pytest.skip(title) document = to_docutils(input, in_sphinx_env=True) - print(document.pformat()) _actual, _expected = [ "\n".join([ll.rstrip() for ll in text.splitlines()]) for text in (document.pformat(), expected) ] - assert _actual == _expected + try: + assert _actual == _expected + except AssertionError: + print(document.pformat()) + raise @pytest.mark.parametrize(