diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a10c3699..a84a065e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: args: [--config-file=pyproject.toml] additional_dependencies: - importlib_metadata - - myst-parser~=0.18.0 + - myst-parser~=1.0.0 - "sphinx~=5.0" - nbclient - types-PyYAML diff --git a/myst_nb/__init__.py b/myst_nb/__init__.py index 7fd25d88..8b156d3a 100644 --- a/myst_nb/__init__.py +++ b/myst_nb/__init__.py @@ -1,5 +1,5 @@ """A docutils/sphinx parser for Jupyter Notebooks.""" -__version__ = "0.17.2" +__version__ = "0.18.0" def setup(app): diff --git a/myst_nb/core/config.py b/myst_nb/core/config.py index 1d69465a..f8468afa 100644 --- a/myst_nb/core/config.py +++ b/myst_nb/core/config.py @@ -1,6 +1,7 @@ """Configuration for myst-nb.""" import dataclasses as dc from enum import Enum +import sys from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Tuple from myst_parser.config.dc_validators import ( @@ -12,7 +13,13 @@ optional, validate_fields, ) -from typing_extensions import Literal + +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal # noqa: F401 + +from myst_nb.warnings_ import MystNBWarnings def custom_formats_converter(value: dict) -> Dict[str, Tuple[str, dict, bool]]: @@ -130,7 +137,7 @@ def __post_init__(self): default_factory=dict, metadata={ "help": "Custom formats for reading notebook; suffix -> reader", - "docutils_exclude": True, + "omit": ["docutils"], "sections": (Section.global_lvl, Section.read), }, ) @@ -184,7 +191,7 @@ def __post_init__(self): "validator": deep_mapping(instance_of(str), instance_of(str)), "help": "Mapping of kernel name regex to replacement kernel name" "(applied before execution)", - "docutils_exclude": True, + "omit": ["docutils"], "sections": (Section.global_lvl, Section.execute), }, ) @@ -220,7 +227,7 @@ def __post_init__(self): "validator": deep_iterable(instance_of(str)), "help": "Exclude (POSIX) glob patterns for notebooks", "legacy_name": "execution_excludepatterns", - "docutils_exclude": True, + "omit": ["docutils"], "sections": (Section.global_lvl, Section.execute), }, ) @@ -387,7 +394,7 @@ def __post_init__(self): "help": "Overrides for the base render priority of mime types: " "list of (builder name, mime type, priority)", # TODO how to allow this in docutils? - "docutils_exclude": True, + "omit": ["docutils"], "sections": (Section.global_lvl, Section.file_lvl, Section.render), }, repr=False, @@ -454,7 +461,7 @@ def __post_init__(self): metadata={ "validator": deep_mapping(instance_of(str), instance_of((str, int))), "help": "Options for image outputs (class|alt|height|width|scale|align)", - "docutils_exclude": True, + "omit": ["docutils"], # TODO backward-compatible change to "image_options"? "cell_key": "image", "sections": ( @@ -471,7 +478,7 @@ def __post_init__(self): metadata={ "validator": deep_mapping(instance_of(str), instance_of((str, int))), "help": "Options for figure outputs (classes|name|caption|caption_before)", - "docutils_exclude": True, + "omit": ["docutils"], "cell_key": "figure", "sections": ( Section.global_lvl, @@ -505,7 +512,7 @@ def __post_init__(self): instance_of(str), deep_mapping(instance_of(str), instance_of(str)) ), "help": "Javascript to be loaded on pages containing ipywidgets", - "docutils_exclude": True, + "omit": ["docutils"], "sections": (Section.global_lvl, Section.render), }, repr=False, @@ -567,7 +574,7 @@ def get_cell_level_config( self, field_name: str, cell_metadata: Dict[str, Any], - warning_callback: Callable[[str, str], Any], + warning_callback: Callable[[str, MystNBWarnings], Any], ) -> Any: """Get a configuration value at the cell level. @@ -593,7 +600,7 @@ def get_cell_level_config( warning_callback( f"Deprecated `cell_metadata_key` 'render' " f"found, replace with {self.cell_metadata_key!r}", - "cell_metadata_key", + MystNBWarnings.CELL_METADATA_KEY, ) cell_meta = cell_metadata["render"] else: @@ -611,7 +618,10 @@ def get_cell_level_config( field.metadata["validator"](self, field, value) return value except Exception as exc: - warning_callback(f"Cell metadata invalid: {exc}", "cell_config") + warning_callback( + f"Cell metadata invalid: {exc}", + MystNBWarnings.CELL_CONFIG, + ) # default/global/file level should have already been merged return getattr(self, field.name) diff --git a/myst_nb/core/read.py b/myst_nb/core/read.py index ad011049..2b3dae4b 100644 --- a/myst_nb/core/read.py +++ b/myst_nb/core/read.py @@ -312,25 +312,22 @@ class _MockDirective: def _read_fenced_cell(token, cell_index, cell_type): - from myst_parser.parsers.directives import ( - DirectiveParsingError, - parse_directive_text, - ) + from myst_parser.parsers.directives import parse_directive_text - try: - _, options, body_lines, _ = parse_directive_text( - directive_class=_MockDirective, - first_line="", - content=token.content, - validate_options=False, - ) - except DirectiveParsingError as err: + result = parse_directive_text( + directive_class=_MockDirective, + first_line="", + content=token.content, + validate_options=False, + ) + if result.warnings: raise MystMetadataParsingError( "{} cell {} at line {} could not be read: {}".format( - cell_type, cell_index, token.map[0] + 1, err + cell_type, cell_index, token.map[0] + 1, result.warnings[0] ) ) - return options, body_lines + + return result.options, result.body def _read_cell_metadata(token, cell_index): diff --git a/myst_nb/core/render.py b/myst_nb/core/render.py index e28b856b..54f1762d 100644 --- a/myst_nb/core/render.py +++ b/myst_nb/core/render.py @@ -28,8 +28,9 @@ from myst_nb.core.config import NbParserConfig from myst_nb.core.execute import NotebookClientBase -from myst_nb.core.loggers import DEFAULT_LOG_TYPE, LoggerType +from myst_nb.core.loggers import LoggerType # DEFAULT_LOG_TYPE, from myst_nb.core.utils import coalesce_streams +from myst_nb.warnings_ import MystNBWarnings, create_warning if TYPE_CHECKING: from markdown_it.tree import SyntaxTreeNode @@ -57,7 +58,6 @@ class MditRenderMixin: # required by mypy md_options: dict[str, Any] document: nodes.document - create_warning: Any render_children: Any add_line_and_source_path: Any add_line_and_source_path_r: Any @@ -95,8 +95,8 @@ def get_cell_level_config( :param cell_metadata: the metadata for the cell """ - def _callback(msg: str, subtype: str): - self.create_warning(msg, line=line, subtype=subtype) + def _callback(msg: str, subtype: MystNBWarnings): + create_warning(self.document, msg, line=line, subtype=subtype) return self.nb_config.get_cell_level_config(field, cell_metadata, _callback) @@ -222,10 +222,11 @@ def _get_nb_source_code_lexer( # TODO this will create a warning for every cell, but perhaps # it should only be a single warning for the notebook (as previously) # TODO allow user to set default lexer? - self.create_warning( + create_warning( + self.document, f"No source code lexer found for notebook cell {cell_index + 1}", - wtype=DEFAULT_LOG_TYPE, - subtype="lexer", + # wtype=DEFAULT_LOG_TYPE, + subtype=MystNBWarnings.LEXER, line=line, append_to=self.current_node, ) @@ -310,11 +311,6 @@ class MimeData: """Index of the output in the cell""" line: int | None = None """Source line of the cell""" - md_headings: bool = False - """Whether to render headings in text/markdown blocks.""" - # we can only do this if know the content will be rendered into the main body - # of the document, e.g. not inside a container node - # (otherwise it will break the structure of the AST) @property def string(self) -> str: @@ -598,9 +594,7 @@ def render_markdown(self, data: MimeData) -> list[nodes.Element]: fmt = self.renderer.get_cell_level_config( "render_markdown_format", data.cell_metadata, line=data.line ) - return self._render_markdown_base( - data, fmt=fmt, inline=False, allow_headings=data.md_headings - ) + return self._render_markdown_base(data, fmt=fmt, inline=False) def render_text_plain(self, data: MimeData) -> list[nodes.Element]: """Render a notebook text/plain mime data output.""" @@ -753,9 +747,7 @@ def render_markdown_inline(self, data: MimeData) -> list[nodes.Element]: fmt = self.renderer.get_cell_level_config( "render_markdown_format", data.cell_metadata, line=data.line ) - return self._render_markdown_base( - data, fmt=fmt, inline=True, allow_headings=data.md_headings - ) + return self._render_markdown_base(data, fmt=fmt, inline=True) def render_text_plain_inline(self, data: MimeData) -> list[nodes.Element]: """Render a notebook text/plain mime data output.""" @@ -796,7 +788,7 @@ def render_widget_view_inline(self, data: MimeData) -> list[nodes.Element]: return self.render_widget_view(data) def _render_markdown_base( - self, data: MimeData, *, fmt: str, inline: bool, allow_headings: bool + self, data: MimeData, *, fmt: str, inline: bool ) -> list[nodes.Element]: """Base render for a notebook markdown mime output (block or inline).""" psuedo_element = nodes.Element() # element to hold the parsed markdown @@ -832,7 +824,6 @@ def _render_markdown_base( data.string, data.line or 0, inline=inline, - allow_headings=allow_headings, ) finally: # restore the parser @@ -986,11 +977,12 @@ def create_figure_context( caption.source = self.document["source"] caption.line = line elif not (isinstance(first_node, nodes.comment) and len(first_node) == 0): - self.create_warning( + create_warning( + self.document, "Figure caption must be a paragraph or empty comment.", line=line, - wtype=DEFAULT_LOG_TYPE, - subtype="fig_caption", + # wtype=DEFAULT_LOG_TYPE, + subtype=MystNBWarnings.FIG_CAPTION, ) self.current_node.append(figure_node) diff --git a/myst_nb/docutils_.py b/myst_nb/docutils_.py index 9738a0f0..7da9c6db 100644 --- a/myst_nb/docutils_.py +++ b/myst_nb/docutils_.py @@ -14,14 +14,7 @@ from markdown_it.token import Token from markdown_it.tree import SyntaxTreeNode from myst_parser.config.main import MdParserConfig, merge_file_level -from myst_parser.mdit_to_docutils.base import ( - DocutilsRenderer, - create_warning, - token_line, -) -from myst_parser.parsers.docutils_ import ( - DOCUTILS_EXCLUDED_ARGS as DOCUTILS_EXCLUDED_ARGS_MYST, -) +from myst_parser.mdit_to_docutils.base import DocutilsRenderer, token_line from myst_parser.parsers.docutils_ import Parser as MystParser from myst_parser.parsers.docutils_ import create_myst_config, create_myst_settings_spec from myst_parser.parsers.mdit import create_md_parser @@ -32,7 +25,7 @@ from myst_nb import static from myst_nb.core.config import NbParserConfig from myst_nb.core.execute import create_client -from myst_nb.core.loggers import DEFAULT_LOG_TYPE, DocutilsDocLogger +from myst_nb.core.loggers import DocutilsDocLogger # DEFAULT_LOG_TYPE, from myst_nb.core.nb_to_tokens import nb_node_to_dict, notebook_to_tokens from myst_nb.core.read import ( NbReader, @@ -50,6 +43,7 @@ ) from myst_nb.ext.eval import load_eval_docutils from myst_nb.ext.glue import load_glue_docutils +from myst_nb.warnings_ import MystNBWarnings, create_warning DOCUTILS_EXCLUDED_ARGS = list( {f.name for f in NbParserConfig.get_fields() if f.metadata.get("docutils_exclude")} @@ -81,7 +75,7 @@ class Parser(MystParser): settings_spec = ( "MyST-NB options", None, - create_myst_settings_spec(DOCUTILS_EXCLUDED_ARGS, NbParserConfig, "nb_"), + create_myst_settings_spec(NbParserConfig, "nb_"), *MystParser.settings_spec, ) """Runtime settings specification.""" @@ -116,18 +110,14 @@ def _parse(self, inputstring: str, document: nodes.document) -> None: # get markdown parsing configuration try: - md_config = create_myst_config( - document.settings, DOCUTILS_EXCLUDED_ARGS_MYST - ) + md_config = create_myst_config(document.settings) except (TypeError, ValueError) as error: logger.error(f"myst configuration invalid: {error.args[0]}") md_config = MdParserConfig() # get notebook rendering configuration try: - nb_config = create_myst_config( - document.settings, DOCUTILS_EXCLUDED_ARGS, NbParserConfig, "nb_" - ) + nb_config = create_myst_config(document.settings, NbParserConfig, "nb_") except (TypeError, ValueError) as error: logger.error(f"myst-nb configuration invalid: {error.args[0]}") nb_config = NbParserConfig() @@ -310,13 +300,14 @@ def _render_nb_cell_code_outputs( mime_type = next(x for x in mime_priority if x in output["data"]) except StopIteration: if output["data"]: - self.create_warning( + create_warning( + self.document, "No output mime type found from render_priority " f"(cell<{cell_index}>.output<{output_index}>", line=line, append_to=self.current_node, - wtype=DEFAULT_LOG_TYPE, - subtype="mime_type", + # wtype=DEFAULT_LOG_TYPE, + subtype=MystNBWarnings.MIME_TYPE, ) else: figure_options = ( @@ -341,12 +332,13 @@ def _render_nb_cell_code_outputs( self.current_node.extend(_nodes) self.add_line_and_source_path_r(_nodes, token) else: - self.create_warning( + create_warning( + self.document, f"Unsupported output type: {output.output_type}", line=line, append_to=self.current_node, - wtype=DEFAULT_LOG_TYPE, - subtype="output_type", + # wtype=DEFAULT_LOG_TYPE, + subtype=MystNBWarnings.OUTPUT_TYPE, ) diff --git a/myst_nb/ext/glue/directives.py b/myst_nb/ext/glue/directives.py index fc63376c..4d42c15a 100644 --- a/myst_nb/ext/glue/directives.py +++ b/myst_nb/ext/glue/directives.py @@ -106,7 +106,6 @@ def run(self) -> List[nodes.Node]: }, output_metadata=result.metadata, line=self.line, - md_headings=True, ) _nodes = result.nb_renderer.render_markdown(mime) self.set_source_info(_nodes) diff --git a/myst_nb/sphinx_.py b/myst_nb/sphinx_.py index 62bd9a7c..dba36531 100644 --- a/myst_nb/sphinx_.py +++ b/myst_nb/sphinx_.py @@ -13,7 +13,7 @@ from markdown_it.tree import SyntaxTreeNode from myst_parser.config.main import MdParserConfig, merge_file_level from myst_parser.mdit_to_docutils.base import token_line -from myst_parser.mdit_to_docutils.sphinx_ import SphinxRenderer, create_warning +from myst_parser.mdit_to_docutils.sphinx_ import SphinxRenderer from myst_parser.parsers.mdit import create_md_parser from myst_parser.parsers.sphinx_ import MystParser import nbformat @@ -38,6 +38,7 @@ get_mime_priority, load_renderer, ) +from myst_nb.warnings_ import MystNBWarnings, create_warning SPHINX_LOGGER = sphinx_logging.getLogger(__name__) @@ -301,12 +302,13 @@ def _render_nb_cell_code_outputs( self.add_line_and_source_path_r([mime_bundle], token) self.current_node.append(mime_bundle) else: - self.create_warning( + create_warning( + self.document, f"Unsupported output type: {output.output_type}", line=line, append_to=self.current_node, - wtype=DEFAULT_LOG_TYPE, - subtype="output_type", + # wtype=DEFAULT_LOG_TYPE, + subtype=MystNBWarnings.OUTPUT_TYPE, ) diff --git a/myst_nb/warnings_.py b/myst_nb/warnings_.py new file mode 100644 index 00000000..de8e66ce --- /dev/null +++ b/myst_nb/warnings_.py @@ -0,0 +1,103 @@ +"""Central handling of warnings for the myst-nb extension.""" +from __future__ import annotations + +from enum import Enum +from typing import Sequence + +from docutils import nodes +from myst_parser.warnings_ import MystWarnings +from myst_parser.warnings_ import create_warning as myst_parser_create_warnings + +__all__ = [ + "MystWarnings", + "MystNBWarnings", + "create_warning", +] + + +class MystNBWarnings(Enum): + """MySTNB warning types.""" + + LEXER = "lexer" + """Issue resolving lexer""" + + FIG_CAPTION = "fig_caption" + """Issue resoliving figure caption""" + + MIME_TYPE = "mime_type" + """Issue resolving MIME type""" + OUTPUT_TYPE = "output_type" + """Issue resolving Output type""" + + CELL_METADATA_KEY = "cell_metadata_key" + """Issue with a key in a cell's `metadata` dictionary.""" + CELL_CONFIG = "cell_config" + """Issue with a cell's configuration or metadata.""" + + +def _is_suppressed_warning( + type: str, subtype: str, suppress_warnings: Sequence[str] +) -> bool: + """Check whether the warning is suppressed or not. + + Mirrors: + https://github.com/sphinx-doc/sphinx/blob/47d9035bca9e83d6db30a0726a02dc9265bd66b1/sphinx/util/logging.py + """ + if type is None: + return False + + subtarget: str | None + + for warning_type in suppress_warnings: + if "." in warning_type: + target, subtarget = warning_type.split(".", 1) + else: + target, subtarget = warning_type, None + + if target == type and subtarget in (None, subtype, "*"): + return True + + return False + + +def create_warning( + document: nodes.document, + message: str, + subtype: MystNBWarnings | MystWarnings, + *, + line: int | None = None, + append_to: nodes.Element | None = None, +) -> nodes.system_message | None: + """Generate a warning, logging if it is necessary. + + If the warning type is listed in the ``suppress_warnings`` configuration, + then ``None`` will be returned and no warning logged. + """ + # Pass off Myst Parser warnings to that package + if isinstance(subtype, MystWarnings): + myst_parser_create_warnings( + document=document, + message=message, + subtype=subtype, + line=line, + append_to=append_to, + ) + + wtype = "myst-nb" + # figure out whether to suppress the warning, if sphinx is available, + # it will have been set up by the Sphinx environment, + # otherwise we will use the configuration set by docutils + suppress_warnings: Sequence[str] = [] + try: + suppress_warnings = document.settings.env.app.config.suppress_warnings + except AttributeError: + suppress_warnings = document.settings.myst_suppress_warnings or [] + if _is_suppressed_warning(wtype, subtype.value, suppress_warnings): + return None + + kwargs = {"line": line} if line is not None else {} + message = f"{message} [{wtype}.{subtype.value}]" + msg_node = document.reporter.warning(message, **kwargs) + if append_to is not None: + append_to.append(msg_node) + return msg_node diff --git a/pyproject.toml b/pyproject.toml index f0cafba4..290d31c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "ipython", "jupyter-cache>=0.5,<0.7", "nbclient", # nbclient version pinned by jupyter-client - "myst-parser~=0.18.0", + "myst-parser~=1.0.0", "nbformat~=5.0", "pyyaml", "sphinx>=4,<6", @@ -71,7 +71,7 @@ rtd = [ "alabaster", "altair", "bokeh", - "coconut>=1.4.3,<3.1.0", + "coconut>=1.4.3,<2.3.0", "ipykernel~=5.5", "ipywidgets", "jupytext>=1.11.2,<1.15.0", @@ -79,7 +79,7 @@ rtd = [ "numpy", "pandas", "plotly", - "sphinx-book-theme>=0.3,<1.1", + "sphinx-book-theme~=1.0.0", "sphinx-copybutton", "sphinx-design~=0.4.0", "sphinxcontrib-bibtex", diff --git a/tests/nb_fixtures/reporter_warnings.txt b/tests/nb_fixtures/reporter_warnings.txt index 813d082c..1e226162 100644 --- a/tests/nb_fixtures/reporter_warnings.txt +++ b/tests/nb_fixtures/reporter_warnings.txt @@ -10,10 +10,9 @@ cells: source: | {unknown}`a` . -:20002: (ERROR/3) Unknown interpreted text role "unknown". +:20002: (WARNING/2) Unknown interpreted text role "unknown". [myst.role_unknown] . - Unknown directive: . cells: @@ -24,7 +23,7 @@ cells: ```{xyz} ``` . -:10003: (ERROR/3) Unknown directive type "xyz". +:10003: (WARNING/2) Unknown directive type: 'xyz' [myst.directive_unknown] . Directive parsing error: @@ -66,5 +65,5 @@ cells: [a]: c . -:20004: (WARNING/2) Duplicate reference definition: A [myst.ref] -. \ No newline at end of file +:20004: (WARNING/2) Duplicate reference definition: A [myst.duplicate_def] +.