"""Inline config comment functionality."""
import functools
import logging
import re
import typing as t
from . import _compat as _t, types
logger = logging.getLogger(__name__)
RSTCHECK_CONFIG_COMMENT_REGEX = re.compile(r"\.\. rstcheck: (.*)=(.*)$")
VALID_INLINE_CONFIG_KEYS = (
"ignore-directives",
"ignore-roles",
"ignore-substitutions",
"ignore-languages",
)
ValidInlineConfigKeys = t.Union[
_t.Literal["ignore-directives"],
_t.Literal["ignore-roles"],
_t.Literal["ignore-substitutions"],
_t.Literal["ignore-languages"],
]
RSTCHECK_FLOW_CONTROL_COMMENT_REGEX = re.compile(r"\.\. rstcheck: ([a-z-]*)$")
VALID_INLINE_FLOW_CONTROLS = ("ignore-next-code-block",)
[docs]@functools.lru_cache()
def get_inline_config_from_source(
source: str, source_origin: types.SourceFileOrString, warn_unknown_settings: bool = False
) -> t.List[types.InlineConfig]:
"""Get rstcheck inline configs from source.
Unknown configs are ignored.
:param source: Source to get config from
:param source_origin: Origin of the source with the inline ignore comments
:param warn_unknown_settings: If a warning should be logged on unknown settings;
defaults to :py:obj:`False`
:return: A list of inline configs
"""
configs: t.List[types.InlineConfig] = []
for idx, line in enumerate(source.splitlines()):
match = RSTCHECK_CONFIG_COMMENT_REGEX.search(line)
if match is None:
continue
key = match.group(1).strip()
value = match.group(2).strip()
if key not in VALID_INLINE_CONFIG_KEYS:
if warn_unknown_settings:
logger.warning(
f"Unknown inline config '{key}' found. "
f"Source: '{source_origin}' at line {idx + 1}"
)
continue
configs.append(types.InlineConfig(key=key, value=value))
return configs
def _filter_config_and_split_values(
target_config: ValidInlineConfigKeys,
source: str,
source_origin: types.SourceFileOrString,
warn_unknown_settings: bool = False,
) -> t.Generator[str, None, None]:
"""Get specified configs and comma split them.
:param target_config: Config target to filter for
:param source: Source to get config from
:param source_origin: Origin of the source with the inline ignore comments
:param warn_unknown_settings: If a warning should be logged on unknown settings;
defaults to :py:obj:`False`
:return: None
:yield: Single values for the ``target_config``
"""
inline_configs = get_inline_config_from_source(source, source_origin, warn_unknown_settings)
for inline_config in inline_configs:
if inline_config["key"] == target_config:
for language in inline_config["value"].split(","):
yield language.strip()
[docs]def find_ignored_directives(
source: str, source_origin: types.SourceFileOrString, warn_unknown_settings: bool = False
) -> t.Generator[str, None, None]:
"""Search the rst source for rstcheck inline ignore-directives comments.
Directives are ignored via comment.
For example, to ignore directive1, directive2, and directive3:
.. testsetup::
from rstcheck_core.inline_config import find_ignored_directives
>>> list(find_ignored_directives('''
... Example
... =======
...
... .. rstcheck: ignore-directives=directive1,directive3
...
... .. rstcheck: ignore-directives=directive2
... ''', "<string>"))
['directive1', 'directive3', 'directive2']
:param source: Rst source code
:param source_origin: Origin of the source with the inline ignore comments
:return: None
:yield: Found directives to ignore
"""
yield from _filter_config_and_split_values(
"ignore-directives", source, source_origin, warn_unknown_settings
)
[docs]def find_ignored_roles(
source: str, source_origin: types.SourceFileOrString, warn_unknown_settings: bool = False
) -> t.Generator[str, None, None]:
"""Search the rst source for rstcheck inline ignore-roles comments.
Roles are ignored via comment.
For example, to ignore role1, role2, and role3:
.. testsetup::
from rstcheck_core.inline_config import find_ignored_roles
>>> list(find_ignored_roles('''
... Example
... =======
...
... .. rstcheck: ignore-roles=role1,role3
...
... .. rstcheck: ignore-roles=role2
... ''', "<string>"))
['role1', 'role3', 'role2']
:param source: Rst source code
:param source_origin: Origin of the source with the inline ignore comments
:return: None
:yield: Found roles to ignore
"""
yield from _filter_config_and_split_values(
"ignore-roles", source, source_origin, warn_unknown_settings
)
[docs]def find_ignored_substitutions(
source: str, source_origin: types.SourceFileOrString, warn_unknown_settings: bool = False
) -> t.Generator[str, None, None]:
"""Search the rst source for rstcheck inline ignore-substitutions comments.
Substitutions are ignored via comment.
For example, to ignore substitution1, substitution2, and substitution3:
.. testsetup::
from rstcheck_core.inline_config import find_ignored_substitutions
>>> list(find_ignored_substitutions('''
... Example
... =======
...
... .. rstcheck: ignore-substitutions=substitution1,substitution3
...
... .. rstcheck: ignore-substitutions=substitution2
... ''', "<string>"))
['substitution1', 'substitution3', 'substitution2']
:param source: Rst source code
:param source_origin: Origin of the source with the inline ignore comments
:return: None
:yield: Found substitutions to ignore
"""
yield from _filter_config_and_split_values(
"ignore-substitutions", source, source_origin, warn_unknown_settings
)
[docs]def find_ignored_languages(
source: str, source_origin: types.SourceFileOrString, warn_unknown_settings: bool = False
) -> t.Generator[str, None, None]:
"""Search the rst source for rstcheck inline ignore-languages comments.
Languages are ignored via comment.
For example, to ignore C++, JSON, and Python:
.. testsetup::
from rstcheck_core.inline_config import find_ignored_languages
>>> list(find_ignored_languages('''
... Example
... =======
...
... .. rstcheck: ignore-languages=cpp,json
...
... .. rstcheck: ignore-languages=python
... ''', "<string>"))
['cpp', 'json', 'python']
:param source: Rst source code
:param source_origin: Origin of the source with the inline ignore comments
:return: None
:yield: Found languages to ignore
"""
yield from _filter_config_and_split_values(
"ignore-languages", source, source_origin, warn_unknown_settings
)
[docs]@functools.lru_cache()
def get_inline_flow_control_from_source(
source: str, source_origin: types.SourceFileOrString, warn_unknown_settings: bool = False
) -> t.List[types.InlineFlowControl]:
"""Get rstcheck inline flow control from source.
Unknown flow controls are ignored.
:param source: Source to get config from
:param source_origin: Origin of the source with the inline flow control
:param warn_unknown_settings: If a warning should be logged on unknown settings;
defaults to :py:obj:`False`
:return: A list of inline flow controls
"""
configs: t.List[types.InlineFlowControl] = []
for idx, line in enumerate(source.splitlines()):
match = RSTCHECK_FLOW_CONTROL_COMMENT_REGEX.search(line)
if match is None:
continue
value = match.group(1).strip()
line_number = idx + 1
if value not in VALID_INLINE_FLOW_CONTROLS:
if warn_unknown_settings:
logger.warning(
f"Unknown inline flow control '{value}' found. "
f"Source: '{source_origin}' at line {line_number}"
)
continue
configs.append(types.InlineFlowControl(value=value, line_number=line_number))
return configs
[docs]def find_code_block_ignore_lines(
source: str,
source_origin: types.SourceFileOrString,
warn_unknown_settings: bool = False,
) -> t.Generator[int, None, None]:
"""Get lines of ``ignore-next-code-block`` flow control comments.
:param source: Source to get config from
:param source_origin: Origin of the source with the inline ignore comments
:param warn_unknown_settings: If a warning should be logged on unknown settings;
defaults to :py:obj:`False`
:return: None
:yield: Single values for the ``target_config``
"""
flow_controls = get_inline_flow_control_from_source(
source, source_origin, warn_unknown_settings
)
for flow_control in flow_controls:
if flow_control["value"] == "ignore-next-code-block":
yield flow_control["line_number"]