diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 139ce525..49f45ed2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,3 +44,9 @@ repos: hooks: - id: mypy additional_dependencies: [mdurl] + exclude: > + (?x)^( + benchmarking/.*\.py| + docs/.*\.py| + scripts/.*\.py| + )$ diff --git a/docs/conf.py b/docs/conf.py index 08a6e78a..e0a6e621 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,19 +44,20 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] nitpicky = True -nitpick_ignore = [ - ("py:class", "Match"), - ("py:class", "Path"), - ("py:class", "x in the interval [0, 1)."), - ("py:class", "markdown_it.helpers.parse_link_destination._Result"), - ("py:class", "markdown_it.helpers.parse_link_title._Result"), - ("py:class", "MarkdownIt"), - ("py:class", "RuleFunc"), - ("py:class", "_NodeType"), - ("py:class", "typing_extensions.Protocol"), +nitpick_ignore_regex = [ + ("py:.*", name) + for name in ( + "_ItemTV", + ".*_NodeType", + ".*Literal.*", + ".*_Result", + "EnvType", + "RuleFunc", + "Path", + "Ellipsis", + ) ] - # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for diff --git a/markdown_it/_punycode.py b/markdown_it/_punycode.py index 9ad24421..f9baad27 100644 --- a/markdown_it/_punycode.py +++ b/markdown_it/_punycode.py @@ -22,6 +22,7 @@ import codecs import re +from typing import Callable REGEX_SEPARATORS = re.compile(r"[\x2E\u3002\uFF0E\uFF61]") REGEX_NON_ASCII = re.compile(r"[^\0-\x7E]") @@ -32,10 +33,10 @@ def encode(uni: str) -> str: def decode(ascii: str) -> str: - return codecs.decode(ascii, encoding="punycode") # type: ignore[call-overload] + return codecs.decode(ascii, encoding="punycode") # type: ignore -def map_domain(string, fn): +def map_domain(string: str, fn: Callable[[str], str]) -> str: parts = string.split("@") result = "" if len(parts) > 1: diff --git a/markdown_it/common/normalize_url.py b/markdown_it/common/normalize_url.py index afec9284..a4ebbaae 100644 --- a/markdown_it/common/normalize_url.py +++ b/markdown_it/common/normalize_url.py @@ -68,7 +68,7 @@ def normalizeLinkText(url: str) -> str: GOOD_DATA_RE = re.compile(r"^data:image\/(gif|png|jpeg|webp);") -def validateLink(url: str, validator: Callable | None = None) -> bool: +def validateLink(url: str, validator: Callable[[str], bool] | None = None) -> bool: """Validate URL link is allowed in output. This validator can prohibit more than really needed to prevent XSS. diff --git a/markdown_it/common/utils.py b/markdown_it/common/utils.py index 9b7c4aeb..ed862e74 100644 --- a/markdown_it/common/utils.py +++ b/markdown_it/common/utils.py @@ -1,8 +1,10 @@ """Utilities for parsing source text """ +from __future__ import annotations + import html import re -from typing import Any +from typing import Any, Match, TypeVar from .entities import entities @@ -22,29 +24,12 @@ def charCodeAt(src: str, pos: int) -> Any: return None -# Merge objects -# -def assign(obj): - """Merge objects /*from1, from2, from3, ...*/)""" - raise NotImplementedError - # sources = Array.prototype.slice.call(arguments, 1) - - # sources.forEach(function (source) { - # if (!source) { return; } - - # if (typeof source !== 'object') { - # throw new TypeError(source + 'must be object') - # } - - # Object.keys(source).forEach(function (key) { - # obj[key] = source[key] - # }) - # }) - - # return obj +_ItemTV = TypeVar("_ItemTV") -def arrayReplaceAt(src: list, pos: int, newElements: list) -> list: +def arrayReplaceAt( + src: list[_ItemTV], pos: int, newElements: list[_ItemTV] +) -> list[_ItemTV]: """ Remove element from array and put another array at those position. Useful for some operations with tokens @@ -133,7 +118,7 @@ def unescapeMd(string: str) -> str: def unescapeAll(string: str) -> str: - def replacer_func(match): + def replacer_func(match: Match[str]) -> str: escaped = match.group(1) if escaped: return escaped diff --git a/markdown_it/helpers/parse_link_destination.py b/markdown_it/helpers/parse_link_destination.py index 58b76f3c..d527ce0c 100644 --- a/markdown_it/helpers/parse_link_destination.py +++ b/markdown_it/helpers/parse_link_destination.py @@ -8,7 +8,7 @@ class _Result: __slots__ = ("ok", "pos", "lines", "str") - def __init__(self): + def __init__(self) -> None: self.ok = False self.pos = 0 self.lines = 0 diff --git a/markdown_it/helpers/parse_link_title.py b/markdown_it/helpers/parse_link_title.py index 842c83bc..8f589336 100644 --- a/markdown_it/helpers/parse_link_title.py +++ b/markdown_it/helpers/parse_link_title.py @@ -6,13 +6,13 @@ class _Result: __slots__ = ("ok", "pos", "lines", "str") - def __init__(self): + def __init__(self) -> None: self.ok = False self.pos = 0 self.lines = 0 self.str = "" - def __str__(self): + def __str__(self) -> str: return self.str diff --git a/markdown_it/main.py b/markdown_it/main.py index 7faac5ad..acf8d079 100644 --- a/markdown_it/main.py +++ b/markdown_it/main.py @@ -2,7 +2,7 @@ from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping from contextlib import contextmanager -from typing import Any +from typing import Any, Literal, overload from . import helpers, presets # noqa F401 from .common import normalize_url, utils # noqa F401 @@ -12,7 +12,7 @@ from .renderer import RendererHTML, RendererProtocol from .rules_core.state_core import StateCore from .token import Token -from .utils import OptionsDict +from .utils import EnvType, OptionsDict, OptionsType, PresetType try: import linkify_it @@ -20,7 +20,7 @@ linkify_it = None -_PRESETS = { +_PRESETS: dict[str, PresetType] = { "default": presets.default.make(), "js-default": presets.js_default.make(), "zero": presets.zero.make(), @@ -32,8 +32,8 @@ class MarkdownIt: def __init__( self, - config: str | Mapping = "commonmark", - options_update: Mapping | None = None, + config: str | PresetType = "commonmark", + options_update: Mapping[str, Any] | None = None, *, renderer_cls: Callable[[MarkdownIt], RendererProtocol] = RendererHTML, ): @@ -67,6 +67,26 @@ def __init__( def __repr__(self) -> str: return f"{self.__class__.__module__}.{self.__class__.__name__}()" + @overload + def __getitem__(self, name: Literal["inline"]) -> ParserInline: + ... + + @overload + def __getitem__(self, name: Literal["block"]) -> ParserBlock: + ... + + @overload + def __getitem__(self, name: Literal["core"]) -> ParserCore: + ... + + @overload + def __getitem__(self, name: Literal["renderer"]) -> RendererProtocol: + ... + + @overload + def __getitem__(self, name: str) -> Any: + ... + def __getitem__(self, name: str) -> Any: return { "inline": self.inline, @@ -75,7 +95,7 @@ def __getitem__(self, name: str) -> Any: "renderer": self.renderer, }[name] - def set(self, options: MutableMapping) -> None: + def set(self, options: OptionsType) -> None: """Set parser options (in the same format as in constructor). Probably, you will never need it, but you can change options after constructor call. @@ -86,7 +106,7 @@ def set(self, options: MutableMapping) -> None: self.options = OptionsDict(options) def configure( - self, presets: str | Mapping, options_update: Mapping | None = None + self, presets: str | PresetType, options_update: Mapping[str, Any] | None = None ) -> MarkdownIt: """Batch load of all options and component settings. This is an internal method, and you probably will not need it. @@ -108,9 +128,9 @@ def configure( options = config.get("options", {}) or {} if options_update: - options = {**options, **options_update} + options = {**options, **options_update} # type: ignore - self.set(options) + self.set(options) # type: ignore if "components" in config: for name, component in config["components"].items(): @@ -206,7 +226,9 @@ def reset_rules(self) -> Generator[None, None, None]: self[chain].ruler.enableOnly(rules) self.inline.ruler2.enableOnly(chain_rules["inline2"]) - def add_render_rule(self, name: str, function: Callable, fmt: str = "html") -> None: + def add_render_rule( + self, name: str, function: Callable[..., Any], fmt: str = "html" + ) -> None: """Add a rule for rendering a particular Token type. Only applied when ``renderer.__output__ == fmt`` @@ -214,7 +236,9 @@ def add_render_rule(self, name: str, function: Callable, fmt: str = "html") -> N if self.renderer.__output__ == fmt: self.renderer.rules[name] = function.__get__(self.renderer) # type: ignore - def use(self, plugin: Callable, *params, **options) -> MarkdownIt: + def use( + self, plugin: Callable[..., None], *params: Any, **options: Any + ) -> MarkdownIt: """Load specified plugin with given params into current parser instance. (chainable) It's just a sugar to call `plugin(md, params)` with curring. @@ -229,7 +253,7 @@ def func(tokens, idx): plugin(self, *params, **options) return self - def parse(self, src: str, env: MutableMapping | None = None) -> list[Token]: + def parse(self, src: str, env: EnvType | None = None) -> list[Token]: """Parse the source string to a token stream :param src: source string @@ -252,7 +276,7 @@ def parse(self, src: str, env: MutableMapping | None = None) -> list[Token]: self.core.process(state) return state.tokens - def render(self, src: str, env: MutableMapping | None = None) -> Any: + def render(self, src: str, env: EnvType | None = None) -> Any: """Render markdown string into html. It does all magic for you :). :param src: source string @@ -266,7 +290,7 @@ def render(self, src: str, env: MutableMapping | None = None) -> Any: env = {} if env is None else env return self.renderer.render(self.parse(src, env), self.options, env) - def parseInline(self, src: str, env: MutableMapping | None = None) -> list[Token]: + def parseInline(self, src: str, env: EnvType | None = None) -> list[Token]: """The same as [[MarkdownIt.parse]] but skip all block rules. :param src: source string @@ -286,7 +310,7 @@ def parseInline(self, src: str, env: MutableMapping | None = None) -> list[Token self.core.process(state) return state.tokens - def renderInline(self, src: str, env: MutableMapping | None = None) -> Any: + def renderInline(self, src: str, env: EnvType | None = None) -> Any: """Similar to [[MarkdownIt.render]] but for single paragraph content. :param src: source string diff --git a/markdown_it/parser_block.py b/markdown_it/parser_block.py index f331ec54..cd240a8a 100644 --- a/markdown_it/parser_block.py +++ b/markdown_it/parser_block.py @@ -2,20 +2,25 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING, Any from . import rules_block from .ruler import Ruler from .rules_block.state_block import StateBlock from .token import Token +from .utils import EnvType + +if TYPE_CHECKING: + from markdown_it import MarkdownIt LOGGER = logging.getLogger(__name__) -_rules: list[tuple] = [ +_rules: list[tuple[str, Any, list[str]]] = [ # First 2 params - rule name & source. Secondary array - list of rules, # which can be terminated by this one. ("table", rules_block.table, ["paragraph", "reference"]), - ("code", rules_block.code), + ("code", rules_block.code, []), ("fence", rules_block.fence, ["paragraph", "reference", "blockquote", "list"]), ( "blockquote", @@ -24,11 +29,11 @@ ), ("hr", rules_block.hr, ["paragraph", "reference", "blockquote", "list"]), ("list", rules_block.list_block, ["paragraph", "reference", "blockquote"]), - ("reference", rules_block.reference), + ("reference", rules_block.reference, []), ("html_block", rules_block.html_block, ["paragraph", "reference", "blockquote"]), ("heading", rules_block.heading, ["paragraph", "reference", "blockquote"]), - ("lheading", rules_block.lheading), - ("paragraph", rules_block.paragraph), + ("lheading", rules_block.lheading, []), + ("paragraph", rules_block.paragraph, []), ] @@ -39,12 +44,10 @@ class ParserBlock: [[Ruler]] instance. Keep configuration of block rules. """ - def __init__(self): + def __init__(self) -> None: self.ruler = Ruler() - for data in _rules: - name = data[0] - rule = data[1] - self.ruler.push(name, rule, {"alt": data[2] if len(data) > 2 else []}) + for name, rule, alt in _rules: + self.ruler.push(name, rule, {"alt": alt}) def tokenize( self, state: StateBlock, startLine: int, endLine: int, silent: bool = False @@ -96,8 +99,8 @@ def tokenize( def parse( self, src: str, - md, - env, + md: MarkdownIt, + env: EnvType, outTokens: list[Token], ords: tuple[int, ...] | None = None, ) -> list[Token] | None: diff --git a/markdown_it/parser_core.py b/markdown_it/parser_core.py index 32209b32..251b7634 100644 --- a/markdown_it/parser_core.py +++ b/markdown_it/parser_core.py @@ -21,7 +21,7 @@ class ParserCore: - def __init__(self): + def __init__(self) -> None: self.ruler = Ruler() for name, rule in _rules: self.ruler.push(name, rule) diff --git a/markdown_it/parser_inline.py b/markdown_it/parser_inline.py index b61c990b..a8228524 100644 --- a/markdown_it/parser_inline.py +++ b/markdown_it/parser_inline.py @@ -2,10 +2,16 @@ """ from __future__ import annotations +from typing import TYPE_CHECKING + from . import rules_inline from .ruler import RuleFunc, Ruler from .rules_inline.state_inline import StateInline from .token import Token +from .utils import EnvType + +if TYPE_CHECKING: + from markdown_it import MarkdownIt # Parser rules _rules: list[tuple[str, RuleFunc]] = [ @@ -31,7 +37,7 @@ class ParserInline: - def __init__(self): + def __init__(self) -> None: self.ruler = Ruler() for name, rule in _rules: self.ruler.push(name, rule) @@ -114,7 +120,9 @@ def tokenize(self, state: StateInline) -> None: if state.pending: state.pushPending() - def parse(self, src: str, md, env, tokens: list[Token]) -> list[Token]: + def parse( + self, src: str, md: MarkdownIt, env: EnvType, tokens: list[Token] + ) -> list[Token]: """Process input string and push inline tokens into `tokens`""" state = StateInline(src, md, env, tokens) self.tokenize(state) diff --git a/markdown_it/presets/__init__.py b/markdown_it/presets/__init__.py index 16f10e51..22cf74cb 100644 --- a/markdown_it/presets/__init__.py +++ b/markdown_it/presets/__init__.py @@ -1,6 +1,7 @@ __all__ = ("commonmark", "default", "zero", "js_default", "gfm_like") from . import commonmark, default, zero +from ..utils import PresetType js_default = default @@ -16,7 +17,7 @@ class gfm_like: """ @staticmethod - def make(): + def make() -> PresetType: config = commonmark.make() config["components"]["core"]["rules"].append("linkify") config["components"]["block"]["rules"].append("table") diff --git a/markdown_it/presets/commonmark.py b/markdown_it/presets/commonmark.py index e44b66bb..60a39250 100644 --- a/markdown_it/presets/commonmark.py +++ b/markdown_it/presets/commonmark.py @@ -6,9 +6,10 @@ - block: table - inline: strikethrough """ +from ..utils import PresetType -def make(): +def make() -> PresetType: return { "options": { "maxNesting": 20, # Internal protection, recursion limit diff --git a/markdown_it/presets/default.py b/markdown_it/presets/default.py index 59f4855e..c9ab902d 100644 --- a/markdown_it/presets/default.py +++ b/markdown_it/presets/default.py @@ -1,7 +1,8 @@ """markdown-it default options.""" +from ..utils import PresetType -def make(): +def make() -> PresetType: return { "options": { "maxNesting": 100, # Internal protection, recursion limit diff --git a/markdown_it/presets/zero.py b/markdown_it/presets/zero.py index af1d9c7f..fcc5eb3a 100644 --- a/markdown_it/presets/zero.py +++ b/markdown_it/presets/zero.py @@ -2,9 +2,10 @@ "Zero" preset, with nothing enabled. Useful for manual configuring of simple modes. For example, to parse bold/italic only. """ +from ..utils import PresetType -def make(): +def make() -> PresetType: return { "options": { "maxNesting": 20, # Internal protection, recursion limit diff --git a/markdown_it/renderer.py b/markdown_it/renderer.py index 2d784826..4cddbc67 100644 --- a/markdown_it/renderer.py +++ b/markdown_it/renderer.py @@ -7,20 +7,20 @@ class Renderer """ from __future__ import annotations -from collections.abc import MutableMapping, Sequence +from collections.abc import Sequence import inspect from typing import Any, ClassVar, Protocol from .common.utils import escapeHtml, unescapeAll from .token import Token -from .utils import OptionsDict +from .utils import EnvType, OptionsDict class RendererProtocol(Protocol): __output__: ClassVar[str] def render( - self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping + self, tokens: Sequence[Token], options: OptionsDict, env: EnvType ) -> Any: ... @@ -57,7 +57,7 @@ def strong_close(self, tokens, idx, options, env): __output__ = "html" - def __init__(self, parser=None): + def __init__(self, parser: Any = None): self.rules = { k: v for k, v in inspect.getmembers(self, predicate=inspect.ismethod) @@ -65,7 +65,7 @@ def __init__(self, parser=None): } def render( - self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping + self, tokens: Sequence[Token], options: OptionsDict, env: EnvType ) -> str: """Takes token stream and generates HTML. @@ -88,7 +88,7 @@ def render( return result def renderInline( - self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping + self, tokens: Sequence[Token], options: OptionsDict, env: EnvType ) -> str: """The same as ``render``, but for single token of `inline` type. @@ -111,7 +111,7 @@ def renderToken( tokens: Sequence[Token], idx: int, options: OptionsDict, - env: MutableMapping, + env: EnvType, ) -> str: """Default token renderer. @@ -184,7 +184,7 @@ def renderInlineAsText( self, tokens: Sequence[Token] | None, options: OptionsDict, - env: MutableMapping, + env: EnvType, ) -> str: """Special kludge for image `alt` attributes to conform CommonMark spec. @@ -210,7 +210,9 @@ def renderInlineAsText( ################################################### - def code_inline(self, tokens: Sequence[Token], idx: int, options, env) -> str: + def code_inline( + self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType + ) -> str: token = tokens[idx] return ( " str: token = tokens[idx] @@ -242,7 +244,7 @@ def fence( tokens: Sequence[Token], idx: int, options: OptionsDict, - env: MutableMapping, + env: EnvType, ) -> str: token = tokens[idx] info = unescapeAll(token.info).strip() if token.info else "" @@ -294,7 +296,7 @@ def image( tokens: Sequence[Token], idx: int, options: OptionsDict, - env: MutableMapping, + env: EnvType, ) -> str: token = tokens[idx] @@ -308,22 +310,28 @@ def image( return self.renderToken(tokens, idx, options, env) def hardbreak( - self, tokens: Sequence[Token], idx: int, options: OptionsDict, *args + self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType ) -> str: return "
\n" if options.xhtmlOut else "
\n" def softbreak( - self, tokens: Sequence[Token], idx: int, options: OptionsDict, *args + self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType ) -> str: return ( ("
\n" if options.xhtmlOut else "
\n") if options.breaks else "\n" ) - def text(self, tokens: Sequence[Token], idx: int, *args) -> str: + def text( + self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType + ) -> str: return escapeHtml(tokens[idx].content) - def html_block(self, tokens: Sequence[Token], idx: int, *args) -> str: + def html_block( + self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType + ) -> str: return tokens[idx].content - def html_inline(self, tokens: Sequence[Token], idx: int, *args) -> str: + def html_inline( + self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType + ) -> str: return tokens[idx].content diff --git a/markdown_it/ruler.py b/markdown_it/ruler.py index 11b937a0..421666cc 100644 --- a/markdown_it/ruler.py +++ b/markdown_it/ruler.py @@ -17,12 +17,14 @@ class Ruler """ from __future__ import annotations -from collections.abc import Callable, Iterable, MutableMapping +from collections.abc import Callable, Iterable from dataclasses import dataclass, field -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict from markdown_it._compat import DATACLASS_KWARGS +from .utils import EnvType + if TYPE_CHECKING: from markdown_it import MarkdownIt @@ -30,7 +32,7 @@ class Ruler class StateBase: srcCharCode: tuple[int, ...] - def __init__(self, src: str, md: MarkdownIt, env: MutableMapping): + def __init__(self, src: str, md: MarkdownIt, env: EnvType): self.src = src self.env = env self.md = md @@ -49,7 +51,11 @@ def src(self, value: str) -> None: # arguments may or may not exist, based on the rule's type (block, # core, inline). Return type is either `None` or `bool` based on the # rule's type. -RuleFunc = Callable +RuleFunc = Callable # type: ignore + + +class RuleOptionsType(TypedDict, total=False): + alt: list[str] @dataclass(**DATACLASS_KWARGS) @@ -61,7 +67,7 @@ class Rule: class Ruler: - def __init__(self): + def __init__(self) -> None: # List of added rules. self.__rules__: list[Rule] = [] # Cached rule chains. @@ -95,7 +101,9 @@ def __compile__(self) -> None: continue self.__cache__[chain].append(rule.fn) - def at(self, ruleName: str, fn: RuleFunc, options=None): + def at( + self, ruleName: str, fn: RuleFunc, options: RuleOptionsType | None = None + ) -> None: """Replace rule by name with new function & options. :param ruleName: rule name to replace. @@ -111,7 +119,13 @@ def at(self, ruleName: str, fn: RuleFunc, options=None): self.__rules__[index].alt = options.get("alt", []) self.__cache__ = None - def before(self, beforeName: str, ruleName: str, fn: RuleFunc, options=None): + def before( + self, + beforeName: str, + ruleName: str, + fn: RuleFunc, + options: RuleOptionsType | None = None, + ) -> None: """Add new rule to chain before one with given name. :param beforeName: new rule will be added before this one. @@ -127,7 +141,13 @@ def before(self, beforeName: str, ruleName: str, fn: RuleFunc, options=None): self.__rules__.insert(index, Rule(ruleName, True, fn, options.get("alt", []))) self.__cache__ = None - def after(self, afterName: str, ruleName: str, fn: RuleFunc, options=None): + def after( + self, + afterName: str, + ruleName: str, + fn: RuleFunc, + options: RuleOptionsType | None = None, + ) -> None: """Add new rule to chain after one with given name. :param afterName: new rule will be added after this one. @@ -145,7 +165,9 @@ def after(self, afterName: str, ruleName: str, fn: RuleFunc, options=None): ) self.__cache__ = None - def push(self, ruleName: str, fn: RuleFunc, options=None): + def push( + self, ruleName: str, fn: RuleFunc, options: RuleOptionsType | None = None + ) -> None: """Push new rule to the end of chain. :param ruleName: new rule will be added to the end of chain. @@ -156,7 +178,9 @@ def push(self, ruleName: str, fn: RuleFunc, options=None): self.__rules__.append(Rule(ruleName, True, fn, (options or {}).get("alt", []))) self.__cache__ = None - def enable(self, names: str | Iterable[str], ignoreInvalid: bool = False): + def enable( + self, names: str | Iterable[str], ignoreInvalid: bool = False + ) -> list[str]: """Enable rules with given names. :param names: name or list of rule names to enable. @@ -166,7 +190,7 @@ def enable(self, names: str | Iterable[str], ignoreInvalid: bool = False): """ if isinstance(names, str): names = [names] - result = [] + result: list[str] = [] for name in names: idx = self.__find__(name) if (idx < 0) and ignoreInvalid: @@ -178,7 +202,9 @@ def enable(self, names: str | Iterable[str], ignoreInvalid: bool = False): self.__cache__ = None return result - def enableOnly(self, names: str | Iterable[str], ignoreInvalid: bool = False): + def enableOnly( + self, names: str | Iterable[str], ignoreInvalid: bool = False + ) -> list[str]: """Enable rules with given names, and disable everything else. :param names: name or list of rule names to enable. @@ -190,9 +216,11 @@ def enableOnly(self, names: str | Iterable[str], ignoreInvalid: bool = False): names = [names] for rule in self.__rules__: rule.enabled = False - self.enable(names, ignoreInvalid) + return self.enable(names, ignoreInvalid) - def disable(self, names: str | Iterable[str], ignoreInvalid: bool = False): + def disable( + self, names: str | Iterable[str], ignoreInvalid: bool = False + ) -> list[str]: """Disable rules with given names. :param names: name or list of rule names to enable. diff --git a/markdown_it/rules_block/blockquote.py b/markdown_it/rules_block/blockquote.py index 965a9e73..3ca0321c 100644 --- a/markdown_it/rules_block/blockquote.py +++ b/markdown_it/rules_block/blockquote.py @@ -9,7 +9,7 @@ LOGGER = logging.getLogger(__name__) -def blockquote(state: StateBlock, startLine: int, endLine: int, silent: bool): +def blockquote(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: LOGGER.debug( "entering blockquote: %s, %s, %s, %s", state, startLine, endLine, silent ) diff --git a/markdown_it/rules_block/code.py b/markdown_it/rules_block/code.py index a796608d..69bd6bdc 100644 --- a/markdown_it/rules_block/code.py +++ b/markdown_it/rules_block/code.py @@ -6,7 +6,7 @@ LOGGER = logging.getLogger(__name__) -def code(state: StateBlock, startLine: int, endLine: int, silent: bool = False): +def code(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: LOGGER.debug("entering code: %s, %s, %s, %s", state, startLine, endLine, silent) if state.sCount[startLine] - state.blkIndent < 4: diff --git a/markdown_it/rules_block/fence.py b/markdown_it/rules_block/fence.py index 53bc6f2d..2bdd95f8 100644 --- a/markdown_it/rules_block/fence.py +++ b/markdown_it/rules_block/fence.py @@ -6,7 +6,7 @@ LOGGER = logging.getLogger(__name__) -def fence(state: StateBlock, startLine: int, endLine: int, silent: bool): +def fence(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: LOGGER.debug("entering fence: %s, %s, %s, %s", state, startLine, endLine, silent) haveEndMarker = False diff --git a/markdown_it/rules_block/heading.py b/markdown_it/rules_block/heading.py index 064d0702..564e1726 100644 --- a/markdown_it/rules_block/heading.py +++ b/markdown_it/rules_block/heading.py @@ -9,7 +9,7 @@ LOGGER = logging.getLogger(__name__) -def heading(state: StateBlock, startLine: int, endLine: int, silent: bool): +def heading(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: LOGGER.debug("entering heading: %s, %s, %s, %s", state, startLine, endLine, silent) pos = state.bMarks[startLine] + state.tShift[startLine] diff --git a/markdown_it/rules_block/hr.py b/markdown_it/rules_block/hr.py index 953bba23..72ea010d 100644 --- a/markdown_it/rules_block/hr.py +++ b/markdown_it/rules_block/hr.py @@ -10,7 +10,7 @@ LOGGER = logging.getLogger(__name__) -def hr(state: StateBlock, startLine: int, endLine: int, silent: bool): +def hr(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: LOGGER.debug("entering hr: %s, %s, %s, %s", state, startLine, endLine, silent) pos = state.bMarks[startLine] + state.tShift[startLine] diff --git a/markdown_it/rules_block/html_block.py b/markdown_it/rules_block/html_block.py index 31afab76..4831f562 100644 --- a/markdown_it/rules_block/html_block.py +++ b/markdown_it/rules_block/html_block.py @@ -12,7 +12,7 @@ # An array of opening and corresponding closing sequences for html tags, # last argument defines whether it can terminate a paragraph or not -HTML_SEQUENCES: list[tuple[re.Pattern, re.Pattern, bool]] = [ +HTML_SEQUENCES: list[tuple[re.Pattern[str], re.Pattern[str], bool]] = [ ( re.compile(r"^<(script|pre|style|textarea)(?=(\s|>|$))", re.IGNORECASE), re.compile(r"<\/(script|pre|style|textarea)>", re.IGNORECASE), @@ -31,7 +31,7 @@ ] -def html_block(state: StateBlock, startLine: int, endLine: int, silent: bool): +def html_block(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: LOGGER.debug( "entering html_block: %s, %s, %s, %s", state, startLine, endLine, silent ) diff --git a/markdown_it/rules_block/lheading.py b/markdown_it/rules_block/lheading.py index 92632acc..a3806f8e 100644 --- a/markdown_it/rules_block/lheading.py +++ b/markdown_it/rules_block/lheading.py @@ -7,7 +7,7 @@ LOGGER = logging.getLogger(__name__) -def lheading(state: StateBlock, startLine: int, endLine: int, silent: bool): +def lheading(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: LOGGER.debug("entering lheading: %s, %s, %s, %s", state, startLine, endLine, silent) level = None diff --git a/markdown_it/rules_block/list.py b/markdown_it/rules_block/list.py index d9c5e554..1592b599 100644 --- a/markdown_it/rules_block/list.py +++ b/markdown_it/rules_block/list.py @@ -9,7 +9,7 @@ # Search `[-+*][\n ]`, returns next pos after marker on success # or -1 on fail. -def skipBulletListMarker(state: StateBlock, startLine: int): +def skipBulletListMarker(state: StateBlock, startLine: int) -> int: pos = state.bMarks[startLine] + state.tShift[startLine] maximum = state.eMarks[startLine] @@ -34,7 +34,7 @@ def skipBulletListMarker(state: StateBlock, startLine: int): # Search `\d+[.)][\n ]`, returns next pos after marker on success # or -1 on fail. -def skipOrderedListMarker(state: StateBlock, startLine: int): +def skipOrderedListMarker(state: StateBlock, startLine: int) -> int: start = state.bMarks[startLine] + state.tShift[startLine] pos = start maximum = state.eMarks[startLine] @@ -83,7 +83,7 @@ def skipOrderedListMarker(state: StateBlock, startLine: int): return pos -def markTightParagraphs(state: StateBlock, idx: int): +def markTightParagraphs(state: StateBlock, idx: int) -> None: level = state.level + 2 i = idx + 2 @@ -96,7 +96,7 @@ def markTightParagraphs(state: StateBlock, idx: int): i += 1 -def list_block(state: StateBlock, startLine: int, endLine: int, silent: bool): +def list_block(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: LOGGER.debug("entering list: %s, %s, %s, %s", state, startLine, endLine, silent) isTerminatingParagraph = False diff --git a/markdown_it/rules_block/paragraph.py b/markdown_it/rules_block/paragraph.py index fef7edf7..3c7d43d3 100644 --- a/markdown_it/rules_block/paragraph.py +++ b/markdown_it/rules_block/paragraph.py @@ -7,7 +7,7 @@ LOGGER = logging.getLogger(__name__) -def paragraph(state: StateBlock, startLine: int, endLine: int, silent: bool = False): +def paragraph(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: LOGGER.debug( "entering paragraph: %s, %s, %s, %s", state, startLine, endLine, silent ) diff --git a/markdown_it/rules_block/reference.py b/markdown_it/rules_block/reference.py index 39e21eb6..5689064b 100644 --- a/markdown_it/rules_block/reference.py +++ b/markdown_it/rules_block/reference.py @@ -6,7 +6,7 @@ LOGGER = logging.getLogger(__name__) -def reference(state: StateBlock, startLine, _endLine, silent): +def reference(state: StateBlock, startLine: int, _endLine: int, silent: bool) -> bool: LOGGER.debug( "entering reference: %s, %s, %s, %s", state, startLine, _endLine, silent ) diff --git a/markdown_it/rules_block/state_block.py b/markdown_it/rules_block/state_block.py index c5589149..7ddf806c 100644 --- a/markdown_it/rules_block/state_block.py +++ b/markdown_it/rules_block/state_block.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from ..common.utils import isSpace from ..ruler import StateBase from ..token import Token +from ..utils import EnvType if TYPE_CHECKING: from markdown_it.main import MarkdownIt @@ -15,7 +16,7 @@ def __init__( self, src: str, md: MarkdownIt, - env, + env: EnvType, tokens: list[Token], srcCharCode: tuple[int, ...] | None = None, ): @@ -36,11 +37,11 @@ def __init__( self.tokens = tokens - self.bMarks = [] # line begin offsets for fast jumps - self.eMarks = [] # line end offsets for fast jumps + self.bMarks: list[int] = [] # line begin offsets for fast jumps + self.eMarks: list[int] = [] # line end offsets for fast jumps # offsets of the first non-space characters (tabs not expanded) - self.tShift = [] - self.sCount = [] # indents for each line (tabs expanded) + self.tShift: list[int] = [] + self.sCount: list[int] = [] # indents for each line (tabs expanded) # An amount of virtual spaces (tabs expanded) between beginning # of each line (bMarks) and real beginning of that line. @@ -52,7 +53,7 @@ def __init__( # an initial tab length, e.g. bsCount=21 applied to string `\t123` # means first tab should be expanded to 4-21%4 === 3 spaces. # - self.bsCount = [] + self.bsCount: list[int] = [] # block parser variables self.blkIndent = 0 # required block content indent (for example, if we are @@ -115,13 +116,13 @@ def __init__( self.lineMax = len(self.bMarks) - 1 # don't count last fake line - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}" f"(line={self.line},level={self.level},tokens={len(self.tokens)})" ) - def push(self, ttype: str, tag: str, nesting: int) -> Token: + def push(self, ttype: str, tag: str, nesting: Literal[-1, 0, 1]) -> Token: """Push new token to "stream".""" token = Token(ttype, tag, nesting) token.block = True diff --git a/markdown_it/rules_block/table.py b/markdown_it/rules_block/table.py index e3db8584..c432d44f 100644 --- a/markdown_it/rules_block/table.py +++ b/markdown_it/rules_block/table.py @@ -1,4 +1,6 @@ # GFM table, https://github.github.com/gfm/#tables-extension- +from __future__ import annotations + import re from ..common.utils import charCodeAt, isSpace @@ -8,7 +10,7 @@ enclosingPipesRe = re.compile(r"^\||\|$") -def getLine(state: StateBlock, line: int): +def getLine(state: StateBlock, line: int) -> str: pos = state.bMarks[line] + state.tShift[line] maximum = state.eMarks[line] @@ -16,8 +18,8 @@ def getLine(state: StateBlock, line: int): return state.src[pos:maximum] -def escapedSplit(string): - result = [] +def escapedSplit(string: str) -> list[str]: + result: list[str] = [] pos = 0 max = len(string) isEscaped = False @@ -47,7 +49,7 @@ def escapedSplit(string): return result -def table(state: StateBlock, startLine: int, endLine: int, silent: bool): +def table(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: tbodyLines = None # should have at least two lines diff --git a/markdown_it/rules_core/replacements.py b/markdown_it/rules_core/replacements.py index 5e9b7ae7..e5d81c7a 100644 --- a/markdown_it/rules_core/replacements.py +++ b/markdown_it/rules_core/replacements.py @@ -56,7 +56,7 @@ SCOPED_ABBR = {"c": "©", "r": "®", "p": "§", "tm": "™"} -def replaceFn(match: re.Match[str]): +def replaceFn(match: re.Match[str]) -> str: return SCOPED_ABBR[match.group(1).lower()] diff --git a/markdown_it/rules_core/state_core.py b/markdown_it/rules_core/state_core.py index 15b7c605..a938041d 100644 --- a/markdown_it/rules_core/state_core.py +++ b/markdown_it/rules_core/state_core.py @@ -1,10 +1,10 @@ from __future__ import annotations -from collections.abc import MutableMapping from typing import TYPE_CHECKING from ..ruler import StateBase from ..token import Token +from ..utils import EnvType if TYPE_CHECKING: from markdown_it import MarkdownIt @@ -15,9 +15,9 @@ def __init__( self, src: str, md: MarkdownIt, - env: MutableMapping, + env: EnvType, tokens: list[Token] | None = None, - ): + ) -> None: self.src = src self.md = md # link to parser instance self.env = env diff --git a/markdown_it/rules_inline/balance_pairs.py b/markdown_it/rules_inline/balance_pairs.py index 5423b5d6..ce0a0884 100644 --- a/markdown_it/rules_inline/balance_pairs.py +++ b/markdown_it/rules_inline/balance_pairs.py @@ -1,9 +1,11 @@ -# For each opening emphasis-like marker find a matching closing one -# -from .state_inline import StateInline +"""Balance paired characters (*, _, etc) in inline tokens.""" +from __future__ import annotations +from .state_inline import Delimiter, StateInline -def processDelimiters(state: StateInline, delimiters, *args): + +def processDelimiters(state: StateInline, delimiters: list[Delimiter]) -> None: + """For each opening emphasis-like marker find a matching closing one.""" openersBottom = {} maximum = len(delimiters) diff --git a/markdown_it/rules_inline/emphasis.py b/markdown_it/rules_inline/emphasis.py index 5262430b..d21b494c 100644 --- a/markdown_it/rules_inline/emphasis.py +++ b/markdown_it/rules_inline/emphasis.py @@ -1,10 +1,11 @@ # Process *this* and _that_ # +from __future__ import annotations from .state_inline import Delimiter, StateInline -def tokenize(state: StateInline, silent: bool): +def tokenize(state: StateInline, silent: bool) -> bool: """Insert each marker as a separate text token, and add it to delimiter list""" start = state.pos marker = state.srcCharCode[start] @@ -38,7 +39,7 @@ def tokenize(state: StateInline, silent: bool): return True -def _postProcess(state, delimiters): +def _postProcess(state: StateInline, delimiters: list[Delimiter]) -> None: i = len(delimiters) - 1 while i >= 0: startDelim = delimiters[i] @@ -92,7 +93,7 @@ def _postProcess(state, delimiters): i -= 1 -def postProcess(state: StateInline): +def postProcess(state: StateInline) -> None: """Walk through delimiter list and replace text tokens with tags.""" _postProcess(state, state.delimiters) diff --git a/markdown_it/rules_inline/entity.py b/markdown_it/rules_inline/entity.py index 08d271ed..9c4c6a0e 100644 --- a/markdown_it/rules_inline/entity.py +++ b/markdown_it/rules_inline/entity.py @@ -9,7 +9,7 @@ NAMED_RE = re.compile(r"^&([a-z][a-z0-9]{1,31});", re.IGNORECASE) -def entity(state: StateInline, silent: bool): +def entity(state: StateInline, silent: bool) -> bool: pos = state.pos maximum = state.posMax diff --git a/markdown_it/rules_inline/escape.py b/markdown_it/rules_inline/escape.py index 36bd0402..1767e01d 100644 --- a/markdown_it/rules_inline/escape.py +++ b/markdown_it/rules_inline/escape.py @@ -9,7 +9,7 @@ ESCAPED[ord(ch)] = 1 -def escape(state: StateInline, silent: bool): +def escape(state: StateInline, silent: bool) -> bool: pos = state.pos maximum = state.posMax diff --git a/markdown_it/rules_inline/html_inline.py b/markdown_it/rules_inline/html_inline.py index b875e884..6a636684 100644 --- a/markdown_it/rules_inline/html_inline.py +++ b/markdown_it/rules_inline/html_inline.py @@ -3,13 +3,13 @@ from .state_inline import StateInline -def isLetter(ch: int): +def isLetter(ch: int) -> bool: lc = ch | 0x20 # to lower case # /* a */ and /* z */ return (lc >= 0x61) and (lc <= 0x7A) -def html_inline(state: StateInline, silent: bool): +def html_inline(state: StateInline, silent: bool) -> bool: pos = state.pos if not state.md.options.get("html", None): diff --git a/markdown_it/rules_inline/image.py b/markdown_it/rules_inline/image.py index d7215bdf..0cb14ffd 100644 --- a/markdown_it/rules_inline/image.py +++ b/markdown_it/rules_inline/image.py @@ -6,7 +6,7 @@ from .state_inline import StateInline -def image(state: StateInline, silent: bool): +def image(state: StateInline, silent: bool) -> bool: label = None href = "" oldPos = state.pos diff --git a/markdown_it/rules_inline/link.py b/markdown_it/rules_inline/link.py index a6345152..c4548ccd 100644 --- a/markdown_it/rules_inline/link.py +++ b/markdown_it/rules_inline/link.py @@ -4,7 +4,7 @@ from .state_inline import StateInline -def link(state: StateInline, silent: bool): +def link(state: StateInline, silent: bool) -> bool: href = "" title = "" label = None diff --git a/markdown_it/rules_inline/newline.py b/markdown_it/rules_inline/newline.py index 3034e408..4c440579 100644 --- a/markdown_it/rules_inline/newline.py +++ b/markdown_it/rules_inline/newline.py @@ -7,7 +7,7 @@ endSpace = re.compile(r" +$") -def newline(state: StateInline, silent: bool): +def newline(state: StateInline, silent: bool) -> bool: pos = state.pos # /* \n */ diff --git a/markdown_it/rules_inline/state_inline.py b/markdown_it/rules_inline/state_inline.py index 283532cc..7c1cb1f3 100644 --- a/markdown_it/rules_inline/state_inline.py +++ b/markdown_it/rules_inline/state_inline.py @@ -1,14 +1,14 @@ from __future__ import annotations from collections import namedtuple -from collections.abc import MutableMapping from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Literal from .._compat import DATACLASS_KWARGS from ..common.utils import isMdAsciiPunct, isPunctChar, isWhiteSpace from ..ruler import StateBase from ..token import Token +from ..utils import EnvType if TYPE_CHECKING: from markdown_it import MarkdownIt @@ -50,13 +50,13 @@ class Delimiter: class StateInline(StateBase): def __init__( - self, src: str, md: MarkdownIt, env: MutableMapping, outTokens: list[Token] - ): + self, src: str, md: MarkdownIt, env: EnvType, outTokens: list[Token] + ) -> None: self.src = src self.env = env self.md = md self.tokens = outTokens - self.tokens_meta: list[dict | None] = [None] * len(outTokens) + self.tokens_meta: list[dict[str, Any] | None] = [None] * len(outTokens) self.pos = 0 self.posMax = len(self.src) @@ -78,13 +78,13 @@ def __init__( self.backticks: dict[int, int] = {} self.backticksScanned = False - def __repr__(self): + def __repr__(self) -> str: return ( f"{self.__class__.__name__}" f"(pos=[{self.pos} of {self.posMax}], token={len(self.tokens)})" ) - def pushPending(self): + def pushPending(self) -> Token: token = Token("text", "", 0) token.content = self.pending token.level = self.pendingLevel @@ -92,7 +92,7 @@ def pushPending(self): self.pending = "" return token - def push(self, ttype, tag, nesting): + def push(self, ttype: str, tag: str, nesting: Literal[-1, 0, 1]) -> Token: """Push new token to "stream". If pending text exists - flush it as text token """ @@ -121,7 +121,7 @@ def push(self, ttype, tag, nesting): self.tokens_meta.append(token_meta) return token - def scanDelims(self, start, canSplitWord): + def scanDelims(self, start: int, canSplitWord: bool) -> Scanned: """ Scan a sequence of emphasis-like markers, and determine whether it can start an emphasis sequence or end an emphasis sequence. diff --git a/markdown_it/rules_inline/strikethrough.py b/markdown_it/rules_inline/strikethrough.py index 9b062a66..8b080816 100644 --- a/markdown_it/rules_inline/strikethrough.py +++ b/markdown_it/rules_inline/strikethrough.py @@ -4,7 +4,7 @@ from .state_inline import Delimiter, StateInline -def tokenize(state: StateInline, silent: bool): +def tokenize(state: StateInline, silent: bool) -> bool: """Insert each marker as a separate text token, and add it to delimiter list""" start = state.pos marker = state.srcCharCode[start] @@ -52,7 +52,7 @@ def tokenize(state: StateInline, silent: bool): return True -def _postProcess(state: StateInline, delimiters: list[Delimiter]): +def _postProcess(state: StateInline, delimiters: list[Delimiter]) -> None: loneMarkers = [] maximum = len(delimiters) @@ -113,7 +113,7 @@ def _postProcess(state: StateInline, delimiters: list[Delimiter]): state.tokens[i] = token -def postProcess(state: StateInline): +def postProcess(state: StateInline) -> None: """Walk through delimiter list and replace text tokens with tags.""" tokens_meta = state.tokens_meta maximum = len(state.tokens_meta) diff --git a/markdown_it/rules_inline/text.py b/markdown_it/rules_inline/text.py index ec6ee0fa..bdf55310 100644 --- a/markdown_it/rules_inline/text.py +++ b/markdown_it/rules_inline/text.py @@ -1,5 +1,6 @@ # Skip text characters for text token, place those to pending buffer # and increment current pos +from typing import Any from .state_inline import StateInline @@ -12,7 +13,7 @@ # http://spec.commonmark.org/0.15/#ascii-punctuation-character -def isTerminatorChar(ch): +def isTerminatorChar(ch: int) -> bool: return ch in { 0x0A, # /* \n */: 0x21, # /* ! */: @@ -40,7 +41,7 @@ def isTerminatorChar(ch): } -def text(state: StateInline, silent: bool, **args): +def text(state: StateInline, silent: bool, **args: Any) -> bool: pos = state.pos posMax = state.posMax while (pos < posMax) and not isTerminatorChar(state.srcCharCode[pos]): diff --git a/markdown_it/rules_inline/text_collapse.py b/markdown_it/rules_inline/text_collapse.py index 6d0c0ab6..e09289cf 100644 --- a/markdown_it/rules_inline/text_collapse.py +++ b/markdown_it/rules_inline/text_collapse.py @@ -1,7 +1,7 @@ from .state_inline import StateInline -def text_collapse(state: StateInline, *args): +def text_collapse(state: StateInline) -> None: """ Clean up tokens after emphasis and strikethrough postprocessing: merge adjacent text nodes into one and re-calculate all token levels diff --git a/markdown_it/token.py b/markdown_it/token.py index 7a41a784..e3f6c9b9 100644 --- a/markdown_it/token.py +++ b/markdown_it/token.py @@ -2,7 +2,7 @@ from collections.abc import Callable, MutableMapping import dataclasses as dc -from typing import Any +from typing import Any, Literal import warnings from markdown_it._compat import DATACLASS_KWARGS @@ -28,7 +28,7 @@ class Token: tag: str """HTML tag name, e.g. 'p'""" - nesting: int + nesting: Literal[-1, 0, 1] """Level change (number in {-1, 0, 1} set), where: - `1` means the tag is opening - `0` means the tag is self-closing @@ -63,7 +63,7 @@ class Token: - The string value of the item marker for ordered-list "list_item_open" tokens """ - meta: dict = dc.field(default_factory=dict) + meta: dict[Any, Any] = dc.field(default_factory=dict) """A place for plugins to store any arbitrary data""" block: bool = False @@ -76,7 +76,7 @@ class Token: Used for tight lists to hide paragraphs. """ - def __post_init__(self): + def __post_init__(self) -> None: self.attrs = convert_attrs(self.attrs) def attrIndex(self, name: str) -> int: @@ -129,7 +129,7 @@ def as_dict( *, children: bool = True, as_upstream: bool = True, - meta_serializer: Callable[[dict], Any] | None = None, + meta_serializer: Callable[[dict[Any, Any]], Any] | None = None, filter: Callable[[str, Any], bool] | None = None, dict_factory: Callable[..., MutableMapping[str, Any]] = dict, ) -> MutableMapping[str, Any]: diff --git a/markdown_it/tree.py b/markdown_it/tree.py index 09476b22..a39ba32a 100644 --- a/markdown_it/tree.py +++ b/markdown_it/tree.py @@ -9,7 +9,6 @@ from typing import Any, NamedTuple, TypeVar, overload from .token import Token -from .utils import _removesuffix class _NesterTokens(NamedTuple): @@ -51,7 +50,7 @@ def __init__( # Empty list unless a non-empty container, or unnested token that has # children (i.e. inline or img) - self._children: list = [] + self._children: list[Any] = [] if create_root: self._set_children_from_tokens(tokens) @@ -119,7 +118,7 @@ def children(self: _NodeType, value: list[_NodeType]) -> None: @property def parent(self: _NodeType) -> _NodeType | None: - return self._parent + return self._parent # type: ignore @parent.setter def parent(self: _NodeType, value: _NodeType | None) -> None: @@ -314,7 +313,7 @@ def info(self) -> str: return self._attribute_token().info @property - def meta(self) -> dict: + def meta(self) -> dict[Any, Any]: """A place for plugins to store an arbitrary data.""" return self._attribute_token().meta @@ -328,3 +327,14 @@ def hidden(self) -> bool: """If it's true, ignore this element when rendering. Used for tight lists to hide paragraphs.""" return self._attribute_token().hidden + + +def _removesuffix(string: str, suffix: str) -> str: + """Remove a suffix from a string. + + Replace this with str.removesuffix() from stdlib when minimum Python + version is 3.9. + """ + if suffix and string.endswith(suffix): + return string[: -len(suffix)] + return string diff --git a/markdown_it/utils.py b/markdown_it/utils.py index 2ba2995a..a9793720 100644 --- a/markdown_it/utils.py +++ b/markdown_it/utils.py @@ -1,95 +1,160 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import MutableMapping as MutableMappingABC from pathlib import Path +from typing import Any, Callable, Iterable, MutableMapping, TypedDict, cast + +EnvType = MutableMapping[str, Any] # note: could use TypeAlias in python 3.10 +"""Type for the environment sandbox used in parsing and rendering, +which stores mutable variables for use by plugins and rules. +""" + + +class OptionsType(TypedDict): + """Options for parsing.""" + + maxNesting: int + """Internal protection, recursion limit.""" + html: bool + """Enable HTML tags in source.""" + linkify: bool + """Enable autoconversion of URL-like texts to links.""" + typographer: bool + """Enable smartquotes and replacements.""" + quotes: str + """Quote characters.""" + xhtmlOut: bool + """Use '/' to close single tags (
).""" + breaks: bool + """Convert newlines in paragraphs into
.""" + langPrefix: str + """CSS language prefix for fenced blocks.""" + highlight: Callable[[str, str, str], str] | None + """Highlighter function: (content, lang, attrs) -> str.""" + + +class PresetType(TypedDict): + """Preset configuration for markdown-it.""" + + options: OptionsType + """Options for parsing.""" + components: MutableMapping[str, MutableMapping[str, list[str]]] + """Components for parsing and rendering.""" + + +class OptionsDict(MutableMappingABC): # type: ignore + """A dictionary, with attribute access to core markdownit configuration options.""" + # Note: ideally we would probably just remove attribute access entirely, + # but we keep it for backwards compatibility. -class OptionsDict(dict): - """A dictionary, with attribute access to core markdownit configuration options.""" + def __init__(self, options: OptionsType) -> None: + self._options = cast(OptionsType, dict(options)) + + def __getitem__(self, key: str) -> Any: + return self._options[key] # type: ignore[literal-required] + + def __setitem__(self, key: str, value: Any) -> None: + self._options[key] = value # type: ignore[literal-required] + + def __delitem__(self, key: str) -> None: + del self._options[key] # type: ignore + + def __iter__(self) -> Iterable[str]: # type: ignore + return iter(self._options) + + def __len__(self) -> int: + return len(self._options) + + def __repr__(self) -> str: + return repr(self._options) + + def __str__(self) -> str: + return str(self._options) @property def maxNesting(self) -> int: """Internal protection, recursion limit.""" - return self["maxNesting"] + return self._options["maxNesting"] @maxNesting.setter - def maxNesting(self, value: int): - self["maxNesting"] = value + def maxNesting(self, value: int) -> None: + self._options["maxNesting"] = value @property def html(self) -> bool: """Enable HTML tags in source.""" - return self["html"] + return self._options["html"] @html.setter - def html(self, value: bool): - self["html"] = value + def html(self, value: bool) -> None: + self._options["html"] = value @property def linkify(self) -> bool: """Enable autoconversion of URL-like texts to links.""" - return self["linkify"] + return self._options["linkify"] @linkify.setter - def linkify(self, value: bool): - self["linkify"] = value + def linkify(self, value: bool) -> None: + self._options["linkify"] = value @property def typographer(self) -> bool: """Enable smartquotes and replacements.""" - return self["typographer"] + return self._options["typographer"] @typographer.setter - def typographer(self, value: bool): - self["typographer"] = value + def typographer(self, value: bool) -> None: + self._options["typographer"] = value @property def quotes(self) -> str: """Quote characters.""" - return self["quotes"] + return self._options["quotes"] @quotes.setter - def quotes(self, value: str): - self["quotes"] = value + def quotes(self, value: str) -> None: + self._options["quotes"] = value @property def xhtmlOut(self) -> bool: """Use '/' to close single tags (
).""" - return self["xhtmlOut"] + return self._options["xhtmlOut"] @xhtmlOut.setter - def xhtmlOut(self, value: bool): - self["xhtmlOut"] = value + def xhtmlOut(self, value: bool) -> None: + self._options["xhtmlOut"] = value @property def breaks(self) -> bool: """Convert newlines in paragraphs into
.""" - return self["breaks"] + return self._options["breaks"] @breaks.setter - def breaks(self, value: bool): - self["breaks"] = value + def breaks(self, value: bool) -> None: + self._options["breaks"] = value @property def langPrefix(self) -> str: """CSS language prefix for fenced blocks.""" - return self["langPrefix"] + return self._options["langPrefix"] @langPrefix.setter - def langPrefix(self, value: str): - self["langPrefix"] = value + def langPrefix(self, value: str) -> None: + self._options["langPrefix"] = value @property def highlight(self) -> Callable[[str, str, str], str] | None: """Highlighter function: (content, langName, langAttrs) -> escaped HTML.""" - return self["highlight"] + return self._options["highlight"] @highlight.setter - def highlight(self, value: Callable[[str, str, str], str] | None): - self["highlight"] = value + def highlight(self, value: Callable[[str, str, str], str] | None) -> None: + self._options["highlight"] = value -def read_fixture_file(path: str | Path) -> list[list]: +def read_fixture_file(path: str | Path) -> list[list[Any]]: text = Path(path).read_text(encoding="utf-8") tests = [] section = 0 @@ -109,14 +174,3 @@ def read_fixture_file(path: str | Path) -> list[list]: last_pos = i return tests - - -def _removesuffix(string: str, suffix: str) -> str: - """Remove a suffix from a string. - - Replace this with str.removesuffix() from stdlib when minimum Python - version is 3.9. - """ - if suffix and string.endswith(suffix): - return string[: -len(suffix)] - return string diff --git a/pyproject.toml b/pyproject.toml index da8d9170..acf2a288 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,9 +87,12 @@ force_sort_within_sections = true show_error_codes = true warn_unused_ignores = true warn_redundant_casts = true -no_implicit_optional = true -strict_equality = true -implicit_reexport = false +strict = true + +[[tool.mypy.overrides]] +module = ["tests.*"] +disallow_untyped_calls = false +disallow_untyped_defs = false [[tool.mypy.overrides]] module = ["tests.test_plugins.*", "markdown.*"] diff --git a/tests/test_api/test_main.py b/tests/test_api/test_main.py index 007259e3..c3a9ac8b 100644 --- a/tests/test_api/test_main.py +++ b/tests/test_api/test_main.py @@ -150,7 +150,7 @@ def test_parseInline(): type="inline", tag="", nesting=0, - attrs=None, + attrs={}, map=[0, 1], level=0, children=[ @@ -158,7 +158,7 @@ def test_parseInline(): type="text", tag="", nesting=0, - attrs=None, + attrs={}, map=None, level=0, children=None, @@ -173,7 +173,7 @@ def test_parseInline(): type="softbreak", tag="br", nesting=0, - attrs=None, + attrs={}, map=None, level=0, children=None, @@ -188,7 +188,7 @@ def test_parseInline(): type="softbreak", tag="br", nesting=0, - attrs=None, + attrs={}, map=None, level=0, children=None, @@ -203,7 +203,7 @@ def test_parseInline(): type="text", tag="", nesting=0, - attrs=None, + attrs={}, map=None, level=0, children=None, @@ -239,7 +239,7 @@ def test_emptyStr(): type="inline", tag="", nesting=0, - attrs=None, + attrs={}, map=[0, 1], level=0, children=[], @@ -257,7 +257,7 @@ def test_empty_env(): """Test that an empty `env` is mutated, not copied and mutated.""" md = MarkdownIt() - env = {} + env = {} # type: ignore md.render("[foo]: /url\n[foo]", env) assert "references" in env diff --git a/tests/test_api/test_token.py b/tests/test_api/test_token.py index e3806b50..44035981 100644 --- a/tests/test_api/test_token.py +++ b/tests/test_api/test_token.py @@ -24,7 +24,7 @@ def test_token(): assert token.attrGet("a") == "b" token.attrJoin("a", "c") assert token.attrGet("a") == "b c" - token.attrPush(["x", "y"]) + token.attrPush(("x", "y")) assert token.attrGet("x") == "y" with warnings.catch_warnings(): warnings.simplefilter("ignore") diff --git a/tests/test_linkify.py b/tests/test_linkify.py index 96d506d1..48b1981c 100644 --- a/tests/test_linkify.py +++ b/tests/test_linkify.py @@ -6,6 +6,7 @@ def test_token_levels(): tokens = mdit.parse("www.python.org") inline = tokens[1] assert inline.type == "inline" + assert inline.children link_open = inline.children[0] assert link_open.type == "link_open" link_text = inline.children[1] diff --git a/tests/test_port/test_references.py b/tests/test_port/test_references.py index 75bf7130..97f8a65a 100644 --- a/tests/test_port/test_references.py +++ b/tests/test_port/test_references.py @@ -4,7 +4,7 @@ def test_ref_definitions(): md = MarkdownIt() src = "[a]: abc\n\n[b]: xyz\n\n[b]: ijk" - env = {} + env = {} # type: ignore tokens = md.parse(src, env) assert tokens == [] assert env == { diff --git a/tests/test_tree.py b/tests/test_tree.py index 7a7d605e..c5203b0b 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -20,7 +20,7 @@ def test_property_passthrough(): tree = SyntaxTreeNode(tokens) heading_node = tree.children[0] assert heading_open.tag == heading_node.tag - assert tuple(heading_open.map) == heading_node.map + assert tuple(heading_open.map or ()) == heading_node.map assert heading_open.level == heading_node.level assert heading_open.content == heading_node.content assert heading_open.markup == heading_node.markup @@ -49,11 +49,13 @@ def test_sibling_traverse(): text_node = paragraph_inline_node.children[0] assert text_node.type == "text" strong_node = text_node.next_sibling + assert strong_node assert strong_node.type == "strong" another_text_node = strong_node.next_sibling + assert another_text_node assert another_text_node.type == "text" assert another_text_node.next_sibling is None - assert another_text_node.previous_sibling.previous_sibling == text_node + assert another_text_node.previous_sibling.previous_sibling == text_node # type: ignore assert text_node.previous_sibling is None