From 5664ce2465eaa97326fe765981ff0361a390843a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 14:07:43 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20REFACTOR:=20Consolidat?= =?UTF-8?q?e=20configuration=20into=20SdConfig=20dataclass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All global configuration is declared once on a dataclass with typed, TOML-compatible fields, per-field validators and help text (modelled on myst-parser's MdParserConfig): flat sd_* confvals remain the public interface; modules read via a typed get_sd_config accessor; invalid values warn under design.config and fall back to defaults, with per-entry pruning for sd_custom_directives. --- docs/conf.py | 29 +++- docs/get_started.md | 9 ++ sphinx_design/config.py | 278 +++++++++++++++++++++++++++++++++++++ sphinx_design/extension.py | 3 +- sphinx_design/icons.py | 6 +- sphinx_design/shared.py | 61 ++++---- tests/test_misc.py | 133 ++++++++++++++++++ 7 files changed, 482 insertions(+), 37 deletions(-) create mode 100644 sphinx_design/config.py diff --git a/docs/conf.py b/docs/conf.py index 4b25ff4..248c6f3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,10 @@ """Configuration file for the Sphinx documentation builder.""" +import dataclasses as dc import os +from sphinx_design.config import SdConfig + project = "Sphinx Design" copyright = "2021, Executable Book Project" author = "Executable Book Project" @@ -118,7 +121,31 @@ "html_image", ] + +def _sd_config_options_table() -> str: + """Generate a Markdown table of all sphinx-design configuration options, + from the ``SdConfig`` dataclass fields. + """ + rows = [ + "| Name | Type | Default | Description |", + "| ---- | ---- | ------- | ----------- |", + ] + for field in dc.fields(SdConfig): + default = ( + field.default_factory() + if field.default_factory is not dc.MISSING + else field.default + ) + type_str = field.metadata.get("doc_type", field.type) + rows.append( + f"| `sd_{field.name}` | `{type_str}` | `{default!r}` " + f"| {field.metadata.get('help', '')} |" + ) + return "\n".join(rows) + + myst_substitutions = { + "sd_config_options": _sd_config_options_table(), "loremipsum": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " "Sed iaculis arcu vitae odio gravida congue. Donec porttitor ac risus et condimentum. " "Phasellus bibendum ac risus a sollicitudin. " @@ -129,5 +156,5 @@ "Aliquam sed lectus ac nisl sollicitudin ultricies id at neque. " "Aliquam fringilla odio vitae lorem ornare, sit amet scelerisque orci fringilla. " "Nam sed arcu dignissim, ultrices quam sit amet, commodo ipsum. " - "Etiam quis nunc at ligula tincidunt eleifend." + "Etiam quis nunc at ligula tincidunt eleifend.", } diff --git a/docs/get_started.md b/docs/get_started.md index 16f0f83..f429116 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -34,6 +34,15 @@ The MyST Markdown examples in this documentation assume that certain optional [M ## Configuration +### Global options + +All global configuration options are prefixed with `sd_`, and can be set in your `conf.py`. +Values are always simple, TOML-compatible, data types: + +{{ sd_config_options }} + +### Hiding the page title + To hide the title header of a page, add to the top of the page: ::::{tab-set} diff --git a/sphinx_design/config.py b/sphinx_design/config.py new file mode 100644 index 0000000..fb136b1 --- /dev/null +++ b/sphinx_design/config.py @@ -0,0 +1,278 @@ +"""Central declarative configuration for sphinx-design. + +All global configuration is declared on the :class:`SdConfig` dataclass: +every option is declared once, with its type, default, validator and help text. + +Every value is plain, TOML-compatible, data +(``str``/``bool``/``int``/``list``/``dict`` of primitives), +so that the configuration could also be read from a TOML file, +or be understood by non-Python implementations. + +The values are registered with Sphinx as flat, ``sd_`` prefixed, +configuration values (e.g. ``fontawesome_latex`` -> ``sd_fontawesome_latex``), +which remain the public interface. +Modules should read the validated configuration via :func:`get_sd_config`, +rather than accessing ``config.sd_*`` attributes directly. + +The field validators mirror the approach of +https://github.com/python-attrs/attrs validators: +they take ``(inst, field, value)`` and raise on invalid values. +""" + +from __future__ import annotations + +import dataclasses as dc +from typing import TYPE_CHECKING, Any, Protocol + +from sphinx.util.logging import getLogger + +if TYPE_CHECKING: + from sphinx.application import Sphinx + from sphinx.config import Config + from sphinx.environment import BuildEnvironment + +LOGGER = getLogger(__name__) + +WARNING_TYPE = "design" +"""Type of warnings emitted by sphinx-design (i.e. ``design.``).""" + + +class ValidatorType(Protocol): + """Protocol for a dataclass field validator.""" + + def __call__( + self, inst: Any, field: dc.Field[Any], value: Any, suffix: str = "" + ) -> None: + """Validate the value of a dataclass field, raising if invalid. + + :param inst: The dataclass instance (or None if not yet created). + :param field: The dataclass field. + :param value: The value to validate. + :param suffix: Suffix to append to the field name in error messages. + :raises TypeError | ValueError: If the value is invalid. + """ + + +def validate_field(inst: Any, field: dc.Field[Any], value: Any) -> None: + """Validate the field of a dataclass, + according to a ``validator`` function set in the field metadata. + + :param inst: The dataclass instance (or None if not yet created). + :param field: The dataclass field. + :param value: The value to validate. + :raises TypeError | ValueError: If the value is invalid. + """ + if "validator" in field.metadata: + field.metadata["validator"](inst, field, value) + + +def validate_fields(inst: Any) -> None: + """Validate the fields of a dataclass instance, + according to ``validator`` functions set in the field metadata. + + This function should be called in the ``__post_init__`` of the dataclass. + + :param inst: The dataclass instance. + :raises TypeError | ValueError: If any value is invalid. + """ + for field in dc.fields(inst): + validate_field(inst, field, getattr(inst, field.name)) + + +def instance_of(type_: type[Any] | tuple[type[Any], ...]) -> ValidatorType: + """Create a validator that raises a ``TypeError`` + if the value is not an instance of the given type(s). + + :param type_: The type(s) to check for. + """ + + def _validator( + inst: Any, field: dc.Field[Any], value: Any, suffix: str = "" + ) -> None: + if not isinstance(value, type_): + raise TypeError( + f"'{field.name}{suffix}' must be of type {type_!r} " + f"(got {value!r} that is a {value.__class__!r})." + ) + + return _validator + + +def validate_custom_directive(field: dc.Field[Any], name: Any, data: Any) -> None: + """Validate the shape of a single custom directive (name -> data) entry. + + Note, whether ``data["inherit"]`` refers to a known sphinx-design directive, + and whether the option names are known for that directive, + can only be checked at registration time + (see ``sphinx_design.shared.setup_custom_directives``). + + :param field: The dataclass field the entry belongs to. + :param name: The name of the new directive. + :param data: The directive data, expected shape + ``{inherit: str, argument: str, options: {str: str}}``. + :raises TypeError | ValueError: If the entry is invalid. + """ + if not isinstance(name, str): + raise TypeError(f"key must be a string: {name!r}") + if not isinstance(data, dict): + raise TypeError(f"{name!r} value must be a dictionary") + if "inherit" not in data: + raise ValueError(f"{name!r} value must have an 'inherit' key") + if not isinstance(data["inherit"], str): + raise TypeError(f"'{name}.inherit' value must be a string") + if "argument" in data and not isinstance(data["argument"], str): + raise TypeError(f"'{name}.argument' value must be a string") + if "options" in data: + if not isinstance(data["options"], dict): + raise TypeError(f"'{name}.options' value must be a dictionary") + for key, value in data["options"].items(): + if not isinstance(key, str): + raise TypeError(f"'{name}.options' key must be a string: {key!r}") + if not isinstance(value, str): + raise TypeError(f"'{name}.options.{key}' value must be a string") + + +def validate_custom_directives( + inst: Any, field: dc.Field[Any], value: Any, suffix: str = "" +) -> None: + """Validate the custom directives mapping, raising on the first invalid entry. + + :param inst: The dataclass instance (or None if not yet created). + :param field: The dataclass field. + :param value: The value to validate. + :param suffix: Suffix to append to the field name in error messages. + :raises TypeError | ValueError: If the value is invalid. + """ + if not isinstance(value, dict): + raise TypeError(f"'{field.name}{suffix}' must be a dictionary (got {value!r})") + for name, data in value.items(): + validate_custom_directive(field, name, data) + + +@dc.dataclass +class SdConfig: + """Global configuration for sphinx-design (all values TOML-compatible). + + In the sphinx configuration, these option names are prepended with ``sd_``. + """ + + custom_directives: dict[str, Any] = dc.field( + default_factory=dict, + metadata={ + "validator": validate_custom_directives, + "entry_validator": validate_custom_directive, + "help": "Custom directives, inheriting from sphinx-design ones", + "doc_type": "dict[str, dict]", + }, + ) + fontawesome_latex: bool = dc.field( + default=False, + metadata={ + "validator": instance_of(bool), + "help": "Render fontawesome icons in LaTeX output", + }, + ) + + def __post_init__(self) -> None: + validate_fields(self) + + @classmethod + def from_sphinx(cls, config: Config) -> SdConfig: + """Create a validated instance from the flat ``sd_`` prefixed + Sphinx configuration values. + + Note, the values are expected to have already been sanitized by + the ``config-inited`` event (which replaces invalid values with defaults), + otherwise this may raise. + + :param config: The Sphinx configuration. + :raises TypeError | ValueError: If any value is invalid. + """ + return cls(**{f.name: getattr(config, f"sd_{f.name}") for f in dc.fields(cls)}) + + +def _field_default(field: dc.Field[Any]) -> Any: + """Return the default value for a dataclass field.""" + if field.default_factory is not dc.MISSING: + return field.default_factory() + return field.default + + +def setup_sd_config(app: Sphinx) -> None: + """Set up the sphinx-design configuration handling. + + Each field of :class:`SdConfig` is registered as a flat ``sd_`` + Sphinx configuration value (the public, backwards-compatible, interface). + + :param app: The Sphinx application object. + """ + for field in dc.fields(SdConfig): + app.add_config_value(f"sd_{field.name}", _field_default(field), "env") + # low priority, so that the values are validated + # before any other `config-inited` listener reads them + app.connect("config-inited", _validate_config_values, priority=400) + app.connect("builder-inited", _attach_env_config) + + +def get_sd_config(env: BuildEnvironment) -> SdConfig: + """Get the validated sphinx-design configuration for a build environment. + + :param env: The Sphinx build environment. + """ + try: + return env.sd_config # type: ignore[attr-defined] + except AttributeError: + sd_config = SdConfig.from_sphinx(env.config) + env.sd_config = sd_config # type: ignore[attr-defined] + return sd_config + + +def _validate_config_values(app: Sphinx, config: Config) -> None: + """Validate the flat ``sd_`` prefixed configuration values + (on the ``config-inited`` event). + + Invalid values are replaced by the field default, with a warning, + so that :class:`SdConfig` instances can subsequently always be created. + For mapping fields with an ``entry_validator``, + only the invalid entries are discarded. + """ + + def _warn(msg: str) -> None: + LOGGER.warning(msg, type=WARNING_TYPE, subtype="config") + + for field in dc.fields(SdConfig): + name = f"sd_{field.name}" + value = getattr(config, name) + if entry_validator := field.metadata.get("entry_validator"): + # validate mapping values per entry, discarding invalid entries, + # so that one invalid entry does not invalidate the whole mapping + if not isinstance(value, dict): + _warn(f"{name}: must be a dictionary") + value = _field_default(field) + else: + valid = {} + for key, entry in value.items(): + try: + entry_validator(field, key, entry) + except (TypeError, ValueError) as exc: + _warn(f"{name}: {exc}") + else: + valid[key] = entry + value = valid + else: + try: + validate_field(None, field, value) + except (TypeError, ValueError) as exc: + value = _field_default(field) + _warn(f"{name}: {exc} Reverting to default: {value!r}") + setattr(config, name, value) + + +def _attach_env_config(app: Sphinx) -> None: + """Attach the validated configuration to the build environment + (on the ``builder-inited`` event). + + This is re-created on every build, + so that changes to the configuration are always picked up. + """ + app.env.sd_config = SdConfig.from_sphinx(app.config) # type: ignore[attr-defined] diff --git a/sphinx_design/extension.py b/sphinx_design/extension.py index fe69349..a20915c 100644 --- a/sphinx_design/extension.py +++ b/sphinx_design/extension.py @@ -15,6 +15,7 @@ from .article_info import setup_article_info from .badges_buttons import setup_badges_and_buttons from .cards import setup_cards +from .config import setup_sd_config from .dropdown import setup_dropdown from .grids import setup_grids from .icons import setup_icons @@ -29,6 +30,7 @@ def setup_extension(app: Sphinx) -> None: """Set up the sphinx extension.""" + setup_sd_config(app) app.connect("builder-inited", update_css_js) app.connect("env-updated", update_css_links) # we override container html visitors, to stop the default behaviour @@ -55,7 +57,6 @@ def setup_extension(app: Sphinx) -> None: setup_tabs(app) setup_article_info(app) - app.add_config_value("sd_custom_directives", {}, "env") app.connect( "config-inited", partial(setup_custom_directives, directive_map=directive_map) ) diff --git a/sphinx_design/icons.py b/sphinx_design/icons.py index b769acc..09a153f 100644 --- a/sphinx_design/icons.py +++ b/sphinx_design/icons.py @@ -12,6 +12,7 @@ from . import compiled from ._compat import read_text +from .config import SdConfig, get_sd_config from .shared import WARNING_TYPE, SdDirective logger = logging.getLogger(__name__) @@ -25,7 +26,6 @@ def setup_icons(app: Sphinx) -> None: app.add_role(style, FontawesomeRole(style)) for style in ["regular", "outlined", "round", "sharp", "twotone"]: app.add_role("material-" + style, MaterialRole(style)) - app.add_config_value("sd_fontawesome_latex", False, "env") app.connect("config-inited", add_fontawesome_pkg) app.add_node( fontawesome, @@ -206,13 +206,13 @@ def depart_fontawesome_html(self, node): def add_fontawesome_pkg(app, config): - if app.config.sd_fontawesome_latex: + if SdConfig.from_sphinx(config).fontawesome_latex: app.add_latex_package("fontawesome") def visit_fontawesome_latex(self, node): """Add latex fonteawesome icon, if configured, else warn.""" - if self.config.sd_fontawesome_latex: + if get_sd_config(self.builder.env).fontawesome_latex: self.body.append(f"\\faicon{{{node['icon']}}}") else: logger.warning( diff --git a/sphinx_design/shared.py b/sphinx_design/shared.py index acdf3eb..60c2092 100644 --- a/sphinx_design/shared.py +++ b/sphinx_design/shared.py @@ -12,9 +12,25 @@ from sphinx.util.docutils import SphinxDirective from sphinx.util.logging import getLogger +from .config import WARNING_TYPE, SdConfig, get_sd_config + LOGGER = getLogger(__name__) -WARNING_TYPE = "design" +__all__ = ( + "SEMANTIC_COLORS", + "SKIP_CHILD_TYPES", + "WARNING_TYPE", + "PassthroughTextElement", + "SdDirective", + "create_component", + "is_component", + "is_ignorable_child", + "make_choice", + "margin_option", + "padding_option", + "setup_custom_directives", + "text_align", +) SEMANTIC_COLORS = ( "primary", @@ -34,45 +50,26 @@ def setup_custom_directives( app: Sphinx, config: Config, directive_map: dict[str, SdDirective] ) -> None: - conf_value = config.sd_custom_directives + """Register the custom directives declared in ``sd_custom_directives``. + + The shape of each entry has already been validated on ``config-inited`` + (see ``sphinx_design.config``); + here we additionally check the values against the known directives. + """ - def _warn(msg): + def _warn(msg: str) -> None: LOGGER.warning( f"sd_custom_directives: {msg}", type=WARNING_TYPE, subtype="config" ) - if not isinstance(conf_value, dict): - _warn("must be a dictionary") - config.sd_custom_directives = {} - return - for name, data in conf_value.items(): - if not isinstance(name, str): - _warn(f"key must be a string: {name!r}") - continue - if not isinstance(data, dict): - _warn(f"{name!r} value must be a dictionary") - continue - if "inherit" not in data: - _warn(f"{name!r} value must have an 'inherit' key") - continue + for name, data in SdConfig.from_sphinx(config).custom_directives.items(): if data["inherit"] not in directive_map: _warn(f"'{name}.inherit' is an unknown directive key: {data['inherit']}") continue directive_cls = directive_map[data["inherit"]] - if "options" in data: - if not isinstance(data["options"], dict): - _warn(f"'{name}.options' value must be a dictionary") - continue - if "argument" in data and not isinstance(data["argument"], str): - _warn(f"'{name}.argument' value must be a string") - continue - for key, value in data["options"].items(): - if key not in directive_cls.option_spec: - _warn(f"'{name}.options' unknown key {key!r}") - continue - if not isinstance(value, str): - _warn(f"'{name}.options.{key}' value must be a string") - continue + for key in data.get("options", {}): + if key not in directive_cls.option_spec: + _warn(f"'{name}.options' unknown key {key!r}") app.add_directive(name, directive_cls, override=True) @@ -95,7 +92,7 @@ def run(self) -> list[nodes.Node]: This method should not be overridden, instead override `run_with_defaults`. """ - if data := self.config.sd_custom_directives.get(self.name): + if data := get_sd_config(self.env).custom_directives.get(self.name): if (not self.arguments) and (argument := data.get("argument")): # type: ignore[has-type] self.arguments = [str(argument)] for key, value in data.get("options", {}).items(): diff --git a/tests/test_misc.py b/tests/test_misc.py index fa550ea..de46076 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,9 +1,13 @@ +import dataclasses as dc +import tomllib + from babel.messages.catalog import Catalog from babel.messages.mofile import write_mo from docutils import nodes import pytest from sphinx_design._compat import findall +from sphinx_design.config import SdConfig, get_sd_config from sphinx_design.icons import get_material_icon_data, get_octicon_data from sphinx_design.shared import is_component from sphinx_design.tabs import sd_tab_input @@ -442,3 +446,132 @@ def test_button_i18n_translated(sphinx_builder): ] assert len(badges) == 1 assert badges[0].astext() == "stable" + + +INVALID_CONFIG_VALUES = { + "custom_directives": (["not", "a", "dict"], "must be a dictionary"), + "fontawesome_latex": ("not-a-bool", "must be of type"), +} +"""An invalidly typed value (and expected warning) for every ``SdConfig`` field.""" + + +def _write_index_rst(builder) -> None: + builder.src_path.joinpath("index.rst").write_text( + "Test\n====\n\ncontent\n", encoding="utf8" + ) + + +def test_config_invalid_values_cover_all_fields(): + """Ensure ``INVALID_CONFIG_VALUES`` stays in sync with the ``SdConfig`` fields.""" + assert set(INVALID_CONFIG_VALUES) == {f.name for f in dc.fields(SdConfig)} + + +@pytest.mark.parametrize("field", dc.fields(SdConfig), ids=lambda field: field.name) +def test_config_invalid_type(field, sphinx_builder): + """An invalidly typed config value should emit a ``design.config`` warning, + and fall back to the field default. + """ + value, expected_warning = INVALID_CONFIG_VALUES[field.name] + builder = sphinx_builder( + conf_kwargs={"extensions": ["sphinx_design"], f"sd_{field.name}": value} + ) + _write_index_rst(builder) + builder.build(assert_pass=False) + assert f"sd_{field.name}: " in builder.warnings + assert expected_warning in builder.warnings + default = ( + field.default_factory() + if field.default_factory is not dc.MISSING + else field.default + ) + assert getattr(get_sd_config(builder.app.env), field.name) == default + + +def test_config_custom_directives_invalid_entry(sphinx_builder): + """An invalid ``sd_custom_directives`` entry should emit a ``design.config`` + warning and be discarded, without affecting valid entries. + """ + builder = sphinx_builder( + conf_kwargs={ + "extensions": ["sphinx_design"], + "sd_custom_directives": { + "dropdown-syntax": {"inherit": "dropdown", "argument": "Syntax"}, + "bad-directive": {"argument": "Other"}, + }, + } + ) + _write_index_rst(builder) + builder.build(assert_pass=False) + assert "'bad-directive' value must have an 'inherit' key" in builder.warnings + assert list(get_sd_config(builder.app.env).custom_directives) == ["dropdown-syntax"] + + +def test_config_custom_directives_unknown_inherit(sphinx_builder): + """An unknown ``inherit`` directive should emit a ``design.config`` warning.""" + builder = sphinx_builder( + conf_kwargs={ + "extensions": ["sphinx_design"], + "sd_custom_directives": {"foo": {"inherit": "unknown"}}, + } + ) + _write_index_rst(builder) + builder.build(assert_pass=False) + assert "'foo.inherit' is an unknown directive key: unknown" in builder.warnings + + +def test_config_warnings_suppressible(sphinx_builder): + """Config warnings are emitted with the ``design.config`` type/subtype, + so they can be suppressed via ``suppress_warnings``. + """ + builder = sphinx_builder( + conf_kwargs={ + "extensions": ["sphinx_design"], + "suppress_warnings": ["design.config"], + # note, entry-level invalidity is used here, since top-level type + # mismatches also emit (non-suppressible) sphinx core warnings + "sd_custom_directives": {"foo": {"inherit": "unknown", "argument": 1}}, + } + ) + _write_index_rst(builder) + builder.build() + assert builder.warnings == "" + + +def test_config_strict_validation(): + """Directly instantiating ``SdConfig`` with invalid values should raise.""" + with pytest.raises(TypeError, match="'fontawesome_latex' must be of type"): + SdConfig(fontawesome_latex="not-a-bool") # type: ignore[arg-type] + with pytest.raises(TypeError, match="'custom_directives' must be a dictionary"): + SdConfig(custom_directives="not-a-dict") # type: ignore[arg-type] + with pytest.raises(ValueError, match="must have an 'inherit' key"): + SdConfig(custom_directives={"foo": {}}) + + +def test_config_toml_round_trip(): + """All configuration values are TOML-representable: + a TOML document containing every field should load and validate. + """ + toml_str = """\ + fontawesome_latex = true + + [custom_directives.dropdown-syntax] + inherit = "dropdown" + argument = "Syntax" + + [custom_directives.dropdown-syntax.options] + color = "primary" + icon = "code" + """ + data = tomllib.loads(toml_str) + assert set(data) == {f.name for f in dc.fields(SdConfig)}, ( + "the TOML sample should contain every SdConfig field" + ) + config = SdConfig(**data) + assert config.fontawesome_latex is True + assert config.custom_directives == { + "dropdown-syntax": { + "inherit": "dropdown", + "argument": "Syntax", + "options": {"color": "primary", "icon": "code"}, + } + } From 48fbc180cbe1becc9690260dca8ba25ea98af7e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 14:09:35 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A7=AA=20TEST:=20skip=20myst-parametr?= =?UTF-8?q?ized=20misc=20tests=20without=20myst-parser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rst/myst comment/target tests added in #271 broke the py311-no-myst tox env (not run in CI); guard the myst variants with the same skipif pattern used in test_snippets. --- tests/test_misc.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_misc.py b/tests/test_misc.py index de46076..65e7b71 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -12,6 +12,18 @@ from sphinx_design.shared import is_component from sphinx_design.tabs import sd_tab_input +try: + import myst_parser # noqa: F401 + + MYST_INSTALLED = True +except ImportError: + MYST_INSTALLED = False + +MYST_PARAM = pytest.param( + "myst", + marks=pytest.mark.skipif(not MYST_INSTALLED, reason="myst-parser not installed"), +) + def test_octicons(file_regression): """Test the available octicon names. @@ -121,7 +133,7 @@ def test_tab_set_with_invalid_children( } -@pytest.mark.parametrize("fmt", ["rst", "myst"]) +@pytest.mark.parametrize("fmt", ["rst", MYST_PARAM]) def test_grid_with_comment(fmt, sphinx_builder): """A comment between grid-items should not trigger a warning. @@ -179,7 +191,7 @@ def test_grid_with_comment(fmt, sphinx_builder): } -@pytest.mark.parametrize("fmt", ["rst", "myst"]) +@pytest.mark.parametrize("fmt", ["rst", MYST_PARAM]) def test_card_carousel_with_comment(fmt, sphinx_builder): """A comment between cards should not trigger a warning. @@ -237,7 +249,7 @@ def test_card_carousel_with_comment(fmt, sphinx_builder): } -@pytest.mark.parametrize("fmt", ["rst", "myst"]) +@pytest.mark.parametrize("fmt", ["rst", MYST_PARAM]) def test_tab_set_with_comment(fmt, sphinx_builder): """A comment between tab-items should not trigger a warning. @@ -293,7 +305,7 @@ def test_tab_set_with_comment(fmt, sphinx_builder): } -@pytest.mark.parametrize("fmt", ["rst", "myst"]) +@pytest.mark.parametrize("fmt", ["rst", MYST_PARAM]) def test_tab_set_with_target(fmt, sphinx_builder): """A hyperlink target before a tab-item should be preserved, so that references to it still resolve, and should not trigger a warning. From 75801acae3e8bb5505ae5a4170c87be629359360 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 14:21:17 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A7=AA=20TEST:=20drop=20unused=20type?= =?UTF-8?q?-ignores=20flagged=20by=20the=20pre-commit=20mypy=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_misc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_misc.py b/tests/test_misc.py index 65e7b71..162dee9 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -552,9 +552,9 @@ def test_config_warnings_suppressible(sphinx_builder): def test_config_strict_validation(): """Directly instantiating ``SdConfig`` with invalid values should raise.""" with pytest.raises(TypeError, match="'fontawesome_latex' must be of type"): - SdConfig(fontawesome_latex="not-a-bool") # type: ignore[arg-type] + SdConfig(fontawesome_latex="not-a-bool") with pytest.raises(TypeError, match="'custom_directives' must be a dictionary"): - SdConfig(custom_directives="not-a-dict") # type: ignore[arg-type] + SdConfig(custom_directives="not-a-dict") with pytest.raises(ValueError, match="must have an 'inherit' key"): SdConfig(custom_directives={"foo": {}})