Skip to content

Commit

Permalink
✨ NEW: Add warning types myst.subtype (#313)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chrisjsewell authored Feb 15, 2021
1 parent c868b3a commit 9372cf8
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 71 deletions.
22 changes: 22 additions & 0 deletions docs/using/howto.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
```
122 changes: 72 additions & 50 deletions myst_parser/docutils_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand All @@ -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])
Expand Down Expand Up @@ -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
Expand All @@ -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]):
Expand All @@ -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]
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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"

Expand Down
9 changes: 6 additions & 3 deletions myst_parser/html_to_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions myst_parser/myst_refs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand All @@ -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]
Expand Down
40 changes: 31 additions & 9 deletions myst_parser/sphinx_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/test_renderers/fixtures/containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Nested notes:
<important>
<system_message level="2" line="1" source="notset" type="WARNING">
<paragraph>
comma-separated classes are deprecated, use `:class:` option instead
comma-separated classes are deprecated, use `:class:` option instead [myst.deprecation]
<note classes="other">
<paragraph>
<emphasis>
Expand All @@ -42,7 +42,7 @@ Admonition with title:
<document source="notset">
<system_message level="2" line="1" source="notset" type="WARNING">
<paragraph>
comma-separated classes are deprecated, use `:class:` option instead
comma-separated classes are deprecated, use `:class:` option instead [myst.deprecation]
<admonition classes="other">
<title>
A
Expand Down
4 changes: 2 additions & 2 deletions tests/test_renderers/fixtures/sphinx_directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,15 @@ acks (`sphinx.directives.other.Acks`):
.

--------------------------------
hlist (`sphinx.directives.other.HList`):
SPHINX3.5 hlist (`sphinx.directives.other.HList`):
.
```{hlist}
- item
```
.
<document source="notset">
<hlist>
<hlist ncolumns="2">
<hlistcol>
<bullet_list>
<list_item>
Expand Down
9 changes: 6 additions & 3 deletions tests/test_renderers/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 9372cf8

Please sign in to comment.