Skip to content

Commit 83d66d4

Browse files
authored
🔧 MAINTAIN: Make type checking strict (#267)
and introduce `TypeDict` allowed by Python 3.8+
1 parent 1ea5457 commit 83d66d4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+375
-230
lines changed

.pre-commit-config.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,9 @@ repos:
4444
hooks:
4545
- id: mypy
4646
additional_dependencies: [mdurl]
47+
exclude: >
48+
(?x)^(
49+
benchmarking/.*\.py|
50+
docs/.*\.py|
51+
scripts/.*\.py|
52+
)$

docs/conf.py

+12-11
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,20 @@
4444
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
4545

4646
nitpicky = True
47-
nitpick_ignore = [
48-
("py:class", "Match"),
49-
("py:class", "Path"),
50-
("py:class", "x in the interval [0, 1)."),
51-
("py:class", "markdown_it.helpers.parse_link_destination._Result"),
52-
("py:class", "markdown_it.helpers.parse_link_title._Result"),
53-
("py:class", "MarkdownIt"),
54-
("py:class", "RuleFunc"),
55-
("py:class", "_NodeType"),
56-
("py:class", "typing_extensions.Protocol"),
47+
nitpick_ignore_regex = [
48+
("py:.*", name)
49+
for name in (
50+
"_ItemTV",
51+
".*_NodeType",
52+
".*Literal.*",
53+
".*_Result",
54+
"EnvType",
55+
"RuleFunc",
56+
"Path",
57+
"Ellipsis",
58+
)
5759
]
5860

59-
6061
# -- Options for HTML output -------------------------------------------------
6162

6263
# The theme to use for HTML and HTML Help pages. See the documentation for

markdown_it/_punycode.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import codecs
2424
import re
25+
from typing import Callable
2526

2627
REGEX_SEPARATORS = re.compile(r"[\x2E\u3002\uFF0E\uFF61]")
2728
REGEX_NON_ASCII = re.compile(r"[^\0-\x7E]")
@@ -32,10 +33,10 @@ def encode(uni: str) -> str:
3233

3334

3435
def decode(ascii: str) -> str:
35-
return codecs.decode(ascii, encoding="punycode") # type: ignore[call-overload]
36+
return codecs.decode(ascii, encoding="punycode") # type: ignore
3637

3738

38-
def map_domain(string, fn):
39+
def map_domain(string: str, fn: Callable[[str], str]) -> str:
3940
parts = string.split("@")
4041
result = ""
4142
if len(parts) > 1:

markdown_it/common/normalize_url.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def normalizeLinkText(url: str) -> str:
6868
GOOD_DATA_RE = re.compile(r"^data:image\/(gif|png|jpeg|webp);")
6969

7070

71-
def validateLink(url: str, validator: Callable | None = None) -> bool:
71+
def validateLink(url: str, validator: Callable[[str], bool] | None = None) -> bool:
7272
"""Validate URL link is allowed in output.
7373
7474
This validator can prohibit more than really needed to prevent XSS.

markdown_it/common/utils.py

+8-23
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Utilities for parsing source text
22
"""
3+
from __future__ import annotations
4+
35
import html
46
import re
5-
from typing import Any
7+
from typing import Any, Match, TypeVar
68

79
from .entities import entities
810

@@ -22,29 +24,12 @@ def charCodeAt(src: str, pos: int) -> Any:
2224
return None
2325

2426

25-
# Merge objects
26-
#
27-
def assign(obj):
28-
"""Merge objects /*from1, from2, from3, ...*/)"""
29-
raise NotImplementedError
30-
# sources = Array.prototype.slice.call(arguments, 1)
31-
32-
# sources.forEach(function (source) {
33-
# if (!source) { return; }
34-
35-
# if (typeof source !== 'object') {
36-
# throw new TypeError(source + 'must be object')
37-
# }
38-
39-
# Object.keys(source).forEach(function (key) {
40-
# obj[key] = source[key]
41-
# })
42-
# })
43-
44-
# return obj
27+
_ItemTV = TypeVar("_ItemTV")
4528

4629

47-
def arrayReplaceAt(src: list, pos: int, newElements: list) -> list:
30+
def arrayReplaceAt(
31+
src: list[_ItemTV], pos: int, newElements: list[_ItemTV]
32+
) -> list[_ItemTV]:
4833
"""
4934
Remove element from array and put another array at those position.
5035
Useful for some operations with tokens
@@ -133,7 +118,7 @@ def unescapeMd(string: str) -> str:
133118

134119

135120
def unescapeAll(string: str) -> str:
136-
def replacer_func(match):
121+
def replacer_func(match: Match[str]) -> str:
137122
escaped = match.group(1)
138123
if escaped:
139124
return escaped

markdown_it/helpers/parse_link_destination.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class _Result:
99
__slots__ = ("ok", "pos", "lines", "str")
1010

11-
def __init__(self):
11+
def __init__(self) -> None:
1212
self.ok = False
1313
self.pos = 0
1414
self.lines = 0

markdown_it/helpers/parse_link_title.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
class _Result:
77
__slots__ = ("ok", "pos", "lines", "str")
88

9-
def __init__(self):
9+
def __init__(self) -> None:
1010
self.ok = False
1111
self.pos = 0
1212
self.lines = 0
1313
self.str = ""
1414

15-
def __str__(self):
15+
def __str__(self) -> str:
1616
return self.str
1717

1818

markdown_it/main.py

+39-15
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping
44
from contextlib import contextmanager
5-
from typing import Any
5+
from typing import Any, Literal, overload
66

77
from . import helpers, presets # noqa F401
88
from .common import normalize_url, utils # noqa F401
@@ -12,15 +12,15 @@
1212
from .renderer import RendererHTML, RendererProtocol
1313
from .rules_core.state_core import StateCore
1414
from .token import Token
15-
from .utils import OptionsDict
15+
from .utils import EnvType, OptionsDict, OptionsType, PresetType
1616

1717
try:
1818
import linkify_it
1919
except ModuleNotFoundError:
2020
linkify_it = None
2121

2222

23-
_PRESETS = {
23+
_PRESETS: dict[str, PresetType] = {
2424
"default": presets.default.make(),
2525
"js-default": presets.js_default.make(),
2626
"zero": presets.zero.make(),
@@ -32,8 +32,8 @@
3232
class MarkdownIt:
3333
def __init__(
3434
self,
35-
config: str | Mapping = "commonmark",
36-
options_update: Mapping | None = None,
35+
config: str | PresetType = "commonmark",
36+
options_update: Mapping[str, Any] | None = None,
3737
*,
3838
renderer_cls: Callable[[MarkdownIt], RendererProtocol] = RendererHTML,
3939
):
@@ -67,6 +67,26 @@ def __init__(
6767
def __repr__(self) -> str:
6868
return f"{self.__class__.__module__}.{self.__class__.__name__}()"
6969

70+
@overload
71+
def __getitem__(self, name: Literal["inline"]) -> ParserInline:
72+
...
73+
74+
@overload
75+
def __getitem__(self, name: Literal["block"]) -> ParserBlock:
76+
...
77+
78+
@overload
79+
def __getitem__(self, name: Literal["core"]) -> ParserCore:
80+
...
81+
82+
@overload
83+
def __getitem__(self, name: Literal["renderer"]) -> RendererProtocol:
84+
...
85+
86+
@overload
87+
def __getitem__(self, name: str) -> Any:
88+
...
89+
7090
def __getitem__(self, name: str) -> Any:
7191
return {
7292
"inline": self.inline,
@@ -75,7 +95,7 @@ def __getitem__(self, name: str) -> Any:
7595
"renderer": self.renderer,
7696
}[name]
7797

78-
def set(self, options: MutableMapping) -> None:
98+
def set(self, options: OptionsType) -> None:
7999
"""Set parser options (in the same format as in constructor).
80100
Probably, you will never need it, but you can change options after constructor call.
81101
@@ -86,7 +106,7 @@ def set(self, options: MutableMapping) -> None:
86106
self.options = OptionsDict(options)
87107

88108
def configure(
89-
self, presets: str | Mapping, options_update: Mapping | None = None
109+
self, presets: str | PresetType, options_update: Mapping[str, Any] | None = None
90110
) -> MarkdownIt:
91111
"""Batch load of all options and component settings.
92112
This is an internal method, and you probably will not need it.
@@ -108,9 +128,9 @@ def configure(
108128

109129
options = config.get("options", {}) or {}
110130
if options_update:
111-
options = {**options, **options_update}
131+
options = {**options, **options_update} # type: ignore
112132

113-
self.set(options)
133+
self.set(options) # type: ignore
114134

115135
if "components" in config:
116136
for name, component in config["components"].items():
@@ -206,15 +226,19 @@ def reset_rules(self) -> Generator[None, None, None]:
206226
self[chain].ruler.enableOnly(rules)
207227
self.inline.ruler2.enableOnly(chain_rules["inline2"])
208228

209-
def add_render_rule(self, name: str, function: Callable, fmt: str = "html") -> None:
229+
def add_render_rule(
230+
self, name: str, function: Callable[..., Any], fmt: str = "html"
231+
) -> None:
210232
"""Add a rule for rendering a particular Token type.
211233
212234
Only applied when ``renderer.__output__ == fmt``
213235
"""
214236
if self.renderer.__output__ == fmt:
215237
self.renderer.rules[name] = function.__get__(self.renderer) # type: ignore
216238

217-
def use(self, plugin: Callable, *params, **options) -> MarkdownIt:
239+
def use(
240+
self, plugin: Callable[..., None], *params: Any, **options: Any
241+
) -> MarkdownIt:
218242
"""Load specified plugin with given params into current parser instance. (chainable)
219243
220244
It's just a sugar to call `plugin(md, params)` with curring.
@@ -229,7 +253,7 @@ def func(tokens, idx):
229253
plugin(self, *params, **options)
230254
return self
231255

232-
def parse(self, src: str, env: MutableMapping | None = None) -> list[Token]:
256+
def parse(self, src: str, env: EnvType | None = None) -> list[Token]:
233257
"""Parse the source string to a token stream
234258
235259
:param src: source string
@@ -252,7 +276,7 @@ def parse(self, src: str, env: MutableMapping | None = None) -> list[Token]:
252276
self.core.process(state)
253277
return state.tokens
254278

255-
def render(self, src: str, env: MutableMapping | None = None) -> Any:
279+
def render(self, src: str, env: EnvType | None = None) -> Any:
256280
"""Render markdown string into html. It does all magic for you :).
257281
258282
:param src: source string
@@ -266,7 +290,7 @@ def render(self, src: str, env: MutableMapping | None = None) -> Any:
266290
env = {} if env is None else env
267291
return self.renderer.render(self.parse(src, env), self.options, env)
268292

269-
def parseInline(self, src: str, env: MutableMapping | None = None) -> list[Token]:
293+
def parseInline(self, src: str, env: EnvType | None = None) -> list[Token]:
270294
"""The same as [[MarkdownIt.parse]] but skip all block rules.
271295
272296
:param src: source string
@@ -286,7 +310,7 @@ def parseInline(self, src: str, env: MutableMapping | None = None) -> list[Token
286310
self.core.process(state)
287311
return state.tokens
288312

289-
def renderInline(self, src: str, env: MutableMapping | None = None) -> Any:
313+
def renderInline(self, src: str, env: EnvType | None = None) -> Any:
290314
"""Similar to [[MarkdownIt.render]] but for single paragraph content.
291315
292316
:param src: source string

markdown_it/parser_block.py

+15-12
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,25 @@
22
from __future__ import annotations
33

44
import logging
5+
from typing import TYPE_CHECKING, Any
56

67
from . import rules_block
78
from .ruler import Ruler
89
from .rules_block.state_block import StateBlock
910
from .token import Token
11+
from .utils import EnvType
12+
13+
if TYPE_CHECKING:
14+
from markdown_it import MarkdownIt
1015

1116
LOGGER = logging.getLogger(__name__)
1217

1318

14-
_rules: list[tuple] = [
19+
_rules: list[tuple[str, Any, list[str]]] = [
1520
# First 2 params - rule name & source. Secondary array - list of rules,
1621
# which can be terminated by this one.
1722
("table", rules_block.table, ["paragraph", "reference"]),
18-
("code", rules_block.code),
23+
("code", rules_block.code, []),
1924
("fence", rules_block.fence, ["paragraph", "reference", "blockquote", "list"]),
2025
(
2126
"blockquote",
@@ -24,11 +29,11 @@
2429
),
2530
("hr", rules_block.hr, ["paragraph", "reference", "blockquote", "list"]),
2631
("list", rules_block.list_block, ["paragraph", "reference", "blockquote"]),
27-
("reference", rules_block.reference),
32+
("reference", rules_block.reference, []),
2833
("html_block", rules_block.html_block, ["paragraph", "reference", "blockquote"]),
2934
("heading", rules_block.heading, ["paragraph", "reference", "blockquote"]),
30-
("lheading", rules_block.lheading),
31-
("paragraph", rules_block.paragraph),
35+
("lheading", rules_block.lheading, []),
36+
("paragraph", rules_block.paragraph, []),
3237
]
3338

3439

@@ -39,12 +44,10 @@ class ParserBlock:
3944
[[Ruler]] instance. Keep configuration of block rules.
4045
"""
4146

42-
def __init__(self):
47+
def __init__(self) -> None:
4348
self.ruler = Ruler()
44-
for data in _rules:
45-
name = data[0]
46-
rule = data[1]
47-
self.ruler.push(name, rule, {"alt": data[2] if len(data) > 2 else []})
49+
for name, rule, alt in _rules:
50+
self.ruler.push(name, rule, {"alt": alt})
4851

4952
def tokenize(
5053
self, state: StateBlock, startLine: int, endLine: int, silent: bool = False
@@ -96,8 +99,8 @@ def tokenize(
9699
def parse(
97100
self,
98101
src: str,
99-
md,
100-
env,
102+
md: MarkdownIt,
103+
env: EnvType,
101104
outTokens: list[Token],
102105
ords: tuple[int, ...] | None = None,
103106
) -> list[Token] | None:

markdown_it/parser_core.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222

2323
class ParserCore:
24-
def __init__(self):
24+
def __init__(self) -> None:
2525
self.ruler = Ruler()
2626
for name, rule in _rules:
2727
self.ruler.push(name, rule)

0 commit comments

Comments
 (0)