Skip to content

🔧 MAINTAIN: Make type checking strict #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 31, 2023
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ repos:
hooks:
- id: mypy
additional_dependencies: [mdurl]
exclude: >
(?x)^(
benchmarking/.*\.py|
docs/.*\.py|
scripts/.*\.py|
)$
23 changes: 12 additions & 11 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions markdown_it/_punycode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]")
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion markdown_it/common/normalize_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 8 additions & 23 deletions markdown_it/common/utils.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion markdown_it/helpers/parse_link_destination.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions markdown_it/helpers/parse_link_title.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
54 changes: 39 additions & 15 deletions markdown_it/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -12,15 +12,15 @@
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
except ModuleNotFoundError:
linkify_it = None


_PRESETS = {
_PRESETS: dict[str, PresetType] = {
"default": presets.default.make(),
"js-default": presets.js_default.make(),
"zero": presets.zero.make(),
Expand All @@ -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,
):
Expand Down Expand Up @@ -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,
Expand All @@ -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.

Expand All @@ -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.
Expand All @@ -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():
Expand Down Expand Up @@ -206,15 +226,19 @@ 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``
"""
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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
27 changes: 15 additions & 12 deletions markdown_it/parser_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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, []),
]


Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion markdown_it/parser_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading