diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 77ffff89..e592a769 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['pypy-3.6', '3.6', '3.7', '3.8', '3.9', '3.10'] + python-version: ['pypy-3.7', '3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 @@ -126,3 +126,11 @@ jobs: with: user: __token__ password: ${{ secrets.PYPI_KEY }} + + allgood: + runs-on: ubuntu-latest + needs: + - pre-commit + - tests + steps: + - run: echo "Great success!" diff --git a/markdown_it/cli/parse.py b/markdown_it/cli/parse.py index 353c526f..e159aad2 100644 --- a/markdown_it/cli/parse.py +++ b/markdown_it/cli/parse.py @@ -4,9 +4,11 @@ Parse one or more markdown files, convert each to HTML, and print to stdout. """ +from __future__ import annotations + import argparse +from collections.abc import Iterable, Sequence import sys -from typing import Iterable, Optional, Sequence from markdown_it import __version__ from markdown_it.main import MarkdownIt @@ -15,7 +17,7 @@ version_str = "markdown-it-py [version {}]".format(__version__) -def main(args: Optional[Sequence[str]] = None) -> int: +def main(args: Sequence[str] | None = None) -> int: namespace = parse_args(args) if namespace.filenames: convert(namespace.filenames) @@ -63,7 +65,7 @@ def interactive() -> None: break -def parse_args(args: Optional[Sequence[str]]) -> argparse.Namespace: +def parse_args(args: Sequence[str] | None) -> argparse.Namespace: """Parse input CLI arguments.""" parser = argparse.ArgumentParser( description="Parse one or more markdown files, " diff --git a/markdown_it/common/normalize_url.py b/markdown_it/common/normalize_url.py index d1ab85e3..4ecf2ef4 100644 --- a/markdown_it/common/normalize_url.py +++ b/markdown_it/common/normalize_url.py @@ -1,5 +1,7 @@ +from __future__ import annotations + +from collections.abc import Callable import re -from typing import Callable, Optional from urllib.parse import urlparse, urlunparse, quote, unquote # noqa: F401 import mdurl @@ -67,7 +69,7 @@ def normalizeLinkText(url: str) -> str: GOOD_DATA_RE = re.compile(r"^data:image\/(gif|png|jpeg|webp);") -def validateLink(url: str, validator: Optional[Callable] = None) -> bool: +def validateLink(url: str, validator: Callable | 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/main.py b/markdown_it/main.py index e87a7a44..508b5ce4 100644 --- a/markdown_it/main.py +++ b/markdown_it/main.py @@ -1,16 +1,8 @@ +from __future__ import annotations + from contextlib import contextmanager -from typing import ( - Any, - Callable, - Dict, - Generator, - Iterable, - List, - Mapping, - MutableMapping, - Optional, - Union, -) +from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping +from typing import Any from . import helpers, presets # noqa F401 from .common import normalize_url, utils # noqa F401 @@ -40,10 +32,10 @@ class MarkdownIt: def __init__( self, - config: Union[str, Mapping] = "commonmark", - options_update: Optional[Mapping] = None, + config: str | Mapping = "commonmark", + options_update: Mapping | None = None, *, - renderer_cls: Callable[["MarkdownIt"], RendererProtocol] = RendererHTML, + renderer_cls: Callable[[MarkdownIt], RendererProtocol] = RendererHTML, ): """Main parser class @@ -94,8 +86,8 @@ def set(self, options: MutableMapping) -> None: self.options = OptionsDict(options) def configure( - self, presets: Union[str, Mapping], options_update: Optional[Mapping] = None - ) -> "MarkdownIt": + self, presets: str | Mapping, options_update: Mapping | None = None + ) -> MarkdownIt: """Batch load of all options and component settings. This is an internal method, and you probably will not need it. But if you will - see available presets and data structure @@ -131,7 +123,7 @@ def configure( return self - def get_all_rules(self) -> Dict[str, List[str]]: + def get_all_rules(self) -> dict[str, list[str]]: """Return the names of all active rules.""" rules = { chain: self[chain].ruler.get_all_rules() @@ -140,7 +132,7 @@ def get_all_rules(self) -> Dict[str, List[str]]: rules["inline2"] = self.inline.ruler2.get_all_rules() return rules - def get_active_rules(self) -> Dict[str, List[str]]: + def get_active_rules(self) -> dict[str, list[str]]: """Return the names of all active rules.""" rules = { chain: self[chain].ruler.get_active_rules() @@ -150,8 +142,8 @@ def get_active_rules(self) -> Dict[str, List[str]]: return rules def enable( - self, names: Union[str, Iterable[str]], ignoreInvalid: bool = False - ) -> "MarkdownIt": + self, names: str | Iterable[str], ignoreInvalid: bool = False + ) -> MarkdownIt: """Enable list or rules. (chainable) :param names: rule name or list of rule names to enable. @@ -182,8 +174,8 @@ def enable( return self def disable( - self, names: Union[str, Iterable[str]], ignoreInvalid: bool = False - ) -> "MarkdownIt": + self, names: str | Iterable[str], ignoreInvalid: bool = False + ) -> MarkdownIt: """The same as [[MarkdownIt.enable]], but turn specified rules off. (chainable) :param names: rule name or list of rule names to disable. @@ -222,7 +214,7 @@ 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, *params, **options) -> MarkdownIt: """Load specified plugin with given params into current parser instance. (chainable) It's just a sugar to call `plugin(md, params)` with curring. @@ -237,7 +229,7 @@ def func(tokens, idx): plugin(self, *params, **options) return self - def parse(self, src: str, env: Optional[MutableMapping] = None) -> List[Token]: + def parse(self, src: str, env: MutableMapping | None = None) -> list[Token]: """Parse the source string to a token stream :param src: source string @@ -260,7 +252,7 @@ def parse(self, src: str, env: Optional[MutableMapping] = None) -> List[Token]: self.core.process(state) return state.tokens - def render(self, src: str, env: Optional[MutableMapping] = None) -> Any: + def render(self, src: str, env: MutableMapping | None = None) -> Any: """Render markdown string into html. It does all magic for you :). :param src: source string @@ -274,9 +266,7 @@ def render(self, src: str, env: Optional[MutableMapping] = 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: Optional[MutableMapping] = None - ) -> List[Token]: + def parseInline(self, src: str, env: MutableMapping | None = None) -> list[Token]: """The same as [[MarkdownIt.parse]] but skip all block rules. :param src: source string @@ -296,7 +286,7 @@ def parseInline( self.core.process(state) return state.tokens - def renderInline(self, src: str, env: Optional[MutableMapping] = None) -> Any: + def renderInline(self, src: str, env: MutableMapping | 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 8c0e8ab3..f5768058 100644 --- a/markdown_it/parser_block.py +++ b/markdown_it/parser_block.py @@ -1,6 +1,7 @@ """Block-level tokenizer.""" +from __future__ import annotations + import logging -from typing import List, Optional, Tuple from .ruler import Ruler from .token import Token @@ -10,7 +11,7 @@ LOGGER = logging.getLogger(__name__) -_rules: List[Tuple] = [ +_rules: list[tuple] = [ # First 2 params - rule name & source. Secondary array - list of rules, # which can be terminated by this one. ("table", rules_block.table, ["paragraph", "reference"]), @@ -97,9 +98,9 @@ def parse( src: str, md, env, - outTokens: List[Token], - ords: Optional[Tuple[int, ...]] = None, - ) -> Optional[List[Token]]: + outTokens: list[Token], + ords: tuple[int, ...] | None = None, + ) -> list[Token] | None: """Process input string and push block tokens into `outTokens`.""" if not src: return None diff --git a/markdown_it/parser_core.py b/markdown_it/parser_core.py index 03982b5c..f0c3ad22 100644 --- a/markdown_it/parser_core.py +++ b/markdown_it/parser_core.py @@ -4,14 +4,14 @@ * Top-level rules executor. Glues block/inline parsers and does intermediate * transformations. """ -from typing import List, Tuple +from __future__ import annotations from .ruler import Ruler, RuleFunc from .rules_core.state_core import StateCore from .rules_core import normalize, block, inline, replace, smartquotes, linkify -_rules: List[Tuple[str, RuleFunc]] = [ +_rules: list[tuple[str, RuleFunc]] = [ ("normalize", normalize), ("block", block), ("inline", inline), diff --git a/markdown_it/parser_inline.py b/markdown_it/parser_inline.py index 30fcff98..826665db 100644 --- a/markdown_it/parser_inline.py +++ b/markdown_it/parser_inline.py @@ -1,6 +1,6 @@ """Tokenizes paragraph content. """ -from typing import List, Tuple +from __future__ import annotations from .ruler import Ruler, RuleFunc from .token import Token @@ -8,7 +8,7 @@ from . import rules_inline # Parser rules -_rules: List[Tuple[str, RuleFunc]] = [ +_rules: list[tuple[str, RuleFunc]] = [ ("text", rules_inline.text), ("newline", rules_inline.newline), ("escape", rules_inline.escape), @@ -22,7 +22,7 @@ ("entity", rules_inline.entity), ] -_rules2: List[Tuple[str, RuleFunc]] = [ +_rules2: list[tuple[str, RuleFunc]] = [ ("balance_pairs", rules_inline.link_pairs), ("strikethrough", rules_inline.strikethrough.postProcess), ("emphasis", rules_inline.emphasis.postProcess), @@ -114,7 +114,7 @@ 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, env, 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/renderer.py b/markdown_it/renderer.py index 9e9c7751..88ee36fe 100644 --- a/markdown_it/renderer.py +++ b/markdown_it/renderer.py @@ -5,14 +5,11 @@ class Renderer copy of rules. Those can be rewritten with ease. Also, you can add new rules if you create plugin and adds new token types. """ +from __future__ import annotations + +from collections.abc import MutableMapping, Sequence import inspect -from typing import ( - Any, - ClassVar, - MutableMapping, - Optional, - Sequence, -) +from typing import Any, ClassVar from .common.utils import unescapeAll, escapeHtml from .token import Token @@ -191,7 +188,7 @@ def renderAttrs(token: Token) -> str: def renderInlineAsText( self, - tokens: Optional[Sequence[Token]], + tokens: Sequence[Token] | None, options: OptionsDict, env: MutableMapping, ) -> str: diff --git a/markdown_it/ruler.py b/markdown_it/ruler.py index 997c95dc..6975d2be 100644 --- a/markdown_it/ruler.py +++ b/markdown_it/ruler.py @@ -15,17 +15,10 @@ class Ruler rules control use [[MarkdownIt.disable]], [[MarkdownIt.enable]] and [[MarkdownIt.use]]. """ -from typing import ( - Callable, - Dict, - Iterable, - List, - MutableMapping, - Optional, - Tuple, - TYPE_CHECKING, - Union, -) +from __future__ import annotations + +from collections.abc import Callable, Iterable, MutableMapping +from typing import TYPE_CHECKING import attr if TYPE_CHECKING: @@ -33,9 +26,9 @@ class Ruler class StateBase: - srcCharCode: Tuple[int, ...] + srcCharCode: tuple[int, ...] - def __init__(self, src: str, md: "MarkdownIt", env: MutableMapping): + def __init__(self, src: str, md: MarkdownIt, env: MutableMapping): self.src = src self.env = env self.md = md @@ -62,17 +55,17 @@ class Rule: name: str = attr.ib() enabled: bool = attr.ib() fn: RuleFunc = attr.ib(repr=False) - alt: List[str] = attr.ib() + alt: list[str] = attr.ib() class Ruler: def __init__(self): # List of added rules. - self.__rules__: List[Rule] = [] + self.__rules__: list[Rule] = [] # Cached rule chains. # First level - chain name, '' for default. # Second level - diginal anchor for fast filtering by charcodes. - self.__cache__: Optional[Dict[str, List[RuleFunc]]] = None + self.__cache__: dict[str, list[RuleFunc]] | None = None def __find__(self, name: str) -> int: """Find rule index by name""" @@ -161,7 +154,7 @@ 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: Union[str, Iterable[str]], ignoreInvalid: bool = False): + def enable(self, names: str | Iterable[str], ignoreInvalid: bool = False): """Enable rules with given names. :param names: name or list of rule names to enable. @@ -183,7 +176,7 @@ def enable(self, names: Union[str, Iterable[str]], ignoreInvalid: bool = False): self.__cache__ = None return result - def enableOnly(self, names: Union[str, Iterable[str]], ignoreInvalid: bool = False): + def enableOnly(self, names: str | Iterable[str], ignoreInvalid: bool = False): """Enable rules with given names, and disable everything else. :param names: name or list of rule names to enable. @@ -197,7 +190,7 @@ def enableOnly(self, names: Union[str, Iterable[str]], ignoreInvalid: bool = Fal rule.enabled = False self.enable(names, ignoreInvalid) - def disable(self, names: Union[str, Iterable[str]], ignoreInvalid: bool = False): + def disable(self, names: str | Iterable[str], ignoreInvalid: bool = False): """Disable rules with given names. :param names: name or list of rule names to enable. @@ -219,7 +212,7 @@ def disable(self, names: Union[str, Iterable[str]], ignoreInvalid: bool = False) self.__cache__ = None return result - def getRules(self, chainName: str) -> List[RuleFunc]: + def getRules(self, chainName: str) -> list[RuleFunc]: """Return array of active functions (rules) for given chain name. It analyzes rules configuration, compiles caches if not exists and returns result. @@ -233,10 +226,10 @@ def getRules(self, chainName: str) -> List[RuleFunc]: # Chain can be empty, if rules disabled. But we still have to return Array. return self.__cache__.get(chainName, []) or [] - def get_all_rules(self) -> List[str]: + def get_all_rules(self) -> list[str]: """Return all available rule names.""" return [r.name for r in self.__rules__] - def get_active_rules(self) -> List[str]: + def get_active_rules(self) -> list[str]: """Return the active rule names.""" return [r.name for r in self.__rules__ if r.enabled] diff --git a/markdown_it/rules_block/blockquote.py b/markdown_it/rules_block/blockquote.py index 543c1f9a..52616167 100644 --- a/markdown_it/rules_block/blockquote.py +++ b/markdown_it/rules_block/blockquote.py @@ -1,6 +1,7 @@ # Block quotes +from __future__ import annotations + import logging -from typing import Optional from .state_block import StateBlock from ..common.utils import isSpace @@ -36,7 +37,7 @@ def blockquote(state: StateBlock, startLine: int, endLine: int, silent: bool): initial = offset = state.sCount[startLine] + 1 try: - second_char_code: Optional[int] = state.srcCharCode[pos] + second_char_code: int | None = state.srcCharCode[pos] except IndexError: second_char_code = None @@ -155,7 +156,7 @@ def blockquote(state: StateBlock, startLine: int, endLine: int, silent: bool): initial = offset = state.sCount[nextLine] + 1 try: - next_char: Optional[int] = state.srcCharCode[pos] + next_char: int | None = state.srcCharCode[pos] except IndexError: next_char = None diff --git a/markdown_it/rules_block/heading.py b/markdown_it/rules_block/heading.py index 353520a3..3ccc3059 100644 --- a/markdown_it/rules_block/heading.py +++ b/markdown_it/rules_block/heading.py @@ -1,6 +1,7 @@ """ Atex heading (#, ##, ...) """ +from __future__ import annotations + import logging -from typing import Optional from .state_block import StateBlock from ..common.utils import isSpace @@ -19,7 +20,7 @@ def heading(state: StateBlock, startLine: int, endLine: int, silent: bool): if state.sCount[startLine] - state.blkIndent >= 4: return False - ch: Optional[int] = state.srcCharCode[pos] + ch: int | None = state.srcCharCode[pos] # /* # */ if ch != 0x23 or pos >= maximum: diff --git a/markdown_it/rules_block/html_block.py b/markdown_it/rules_block/html_block.py index 3bb850e5..335d7cd0 100644 --- a/markdown_it/rules_block/html_block.py +++ b/markdown_it/rules_block/html_block.py @@ -1,7 +1,8 @@ # HTML block +from __future__ import annotations + import logging import re -from typing import List, Tuple, Pattern from .state_block import StateBlock from ..common.html_blocks import block_names @@ -11,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[Pattern, Pattern, bool]] = [ +HTML_SEQUENCES: list[tuple[re.Pattern, re.Pattern, bool]] = [ ( re.compile(r"^<(script|pre|style|textarea)(?=(\s|>|$))", re.IGNORECASE), re.compile(r"<\/(script|pre|style|textarea)>", re.IGNORECASE), diff --git a/markdown_it/rules_block/state_block.py b/markdown_it/rules_block/state_block.py index 69ad0c4d..b2c71892 100644 --- a/markdown_it/rules_block/state_block.py +++ b/markdown_it/rules_block/state_block.py @@ -1,4 +1,6 @@ -from typing import List, Optional, Tuple, TYPE_CHECKING +from __future__ import annotations + +from typing import TYPE_CHECKING from ..token import Token from ..ruler import StateBase @@ -12,10 +14,10 @@ class StateBlock(StateBase): def __init__( self, src: str, - md: "MarkdownIt", + md: MarkdownIt, env, - tokens: List[Token], - srcCharCode: Optional[Tuple[int, ...]] = None, + tokens: list[Token], + srcCharCode: tuple[int, ...] | None = None, ): if srcCharCode is not None: diff --git a/markdown_it/rules_core/replacements.py b/markdown_it/rules_core/replacements.py index ee3b9edb..bced7026 100644 --- a/markdown_it/rules_core/replacements.py +++ b/markdown_it/rules_core/replacements.py @@ -14,9 +14,10 @@ * ``--`` → &ndash * ``---`` → &mdash """ +from __future__ import annotations + import logging import re -from typing import List, Match from .state_core import StateCore from ..token import Token @@ -55,11 +56,11 @@ SCOPED_ABBR = {"c": "©", "r": "®", "p": "§", "tm": "™"} -def replaceFn(match: Match[str]): +def replaceFn(match: re.Match[str]): return SCOPED_ABBR[match.group(1).lower()] -def replace_scoped(inlineTokens: List[Token]) -> None: +def replace_scoped(inlineTokens: list[Token]) -> None: inside_autolink = 0 for token in inlineTokens: @@ -73,7 +74,7 @@ def replace_scoped(inlineTokens: List[Token]) -> None: inside_autolink += 1 -def replace_rare(inlineTokens: List[Token]) -> None: +def replace_rare(inlineTokens: list[Token]) -> None: inside_autolink = 0 for token in inlineTokens: diff --git a/markdown_it/rules_core/smartquotes.py b/markdown_it/rules_core/smartquotes.py index c3211191..7c297269 100644 --- a/markdown_it/rules_core/smartquotes.py +++ b/markdown_it/rules_core/smartquotes.py @@ -1,7 +1,9 @@ """Convert straight quotation marks to typographic ones """ +from __future__ import annotations + import re -from typing import Any, Dict, List +from typing import Any from .state_core import StateCore from ..common.utils import charCodeAt @@ -21,8 +23,8 @@ def replaceAt(string: str, index: int, ch: str) -> str: return string[:index] + ch + string[index + 1 :] -def process_inlines(tokens: List[Token], state: StateCore) -> None: - stack: List[Dict[str, Any]] = [] +def process_inlines(tokens: list[Token], state: StateCore) -> None: + stack: list[dict[str, Any]] = [] for i in range(len(tokens)): token = tokens[i] diff --git a/markdown_it/rules_core/state_core.py b/markdown_it/rules_core/state_core.py index a560a283..3521df2f 100644 --- a/markdown_it/rules_core/state_core.py +++ b/markdown_it/rules_core/state_core.py @@ -1,4 +1,7 @@ -from typing import List, MutableMapping, Optional, TYPE_CHECKING +from __future__ import annotations + +from collections.abc import MutableMapping +from typing import TYPE_CHECKING from ..token import Token from ..ruler import StateBase @@ -11,12 +14,12 @@ class StateCore(StateBase): def __init__( self, src: str, - md: "MarkdownIt", + md: MarkdownIt, env: MutableMapping, - tokens: Optional[List[Token]] = None, + tokens: list[Token] | None = None, ): self.src = src self.md = md # link to parser instance self.env = env - self.tokens: List[Token] = tokens or [] + self.tokens: list[Token] = tokens or [] self.inlineMode = False diff --git a/markdown_it/rules_inline/image.py b/markdown_it/rules_inline/image.py index d3813f77..eb3824b1 100644 --- a/markdown_it/rules_inline/image.py +++ b/markdown_it/rules_inline/image.py @@ -1,6 +1,5 @@ # Process ![image]( "title") - -from typing import List +from __future__ import annotations from .state_inline import StateInline from ..token import Token @@ -132,7 +131,7 @@ def image(state: StateInline, silent: bool): if not silent: content = state.src[labelStart:labelEnd] - tokens: List[Token] = [] + tokens: list[Token] = [] state.md.inline.parse(content, state.md, state.env, tokens) token = state.push("image", "img", 0) diff --git a/markdown_it/rules_inline/state_inline.py b/markdown_it/rules_inline/state_inline.py index 54555411..a6f72a92 100644 --- a/markdown_it/rules_inline/state_inline.py +++ b/markdown_it/rules_inline/state_inline.py @@ -1,5 +1,8 @@ +from __future__ import annotations + from collections import namedtuple -from typing import Dict, List, MutableMapping, Optional, TYPE_CHECKING +from collections.abc import MutableMapping +from typing import TYPE_CHECKING import attr @@ -47,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: MutableMapping, outTokens: list[Token] ): self.src = src self.env = env self.md = md self.tokens = outTokens - self.tokens_meta: List[Optional[dict]] = [None] * len(outTokens) + self.tokens_meta: list[dict | None] = [None] * len(outTokens) self.pos = 0 self.posMax = len(self.src) @@ -63,16 +66,16 @@ def __init__( # Stores { start: end } pairs. Useful for backtrack # optimization of pairs parse (emphasis, strikes). - self.cache: Dict[int, int] = {} + self.cache: dict[int, int] = {} # List of emphasis-like delimiters for current tag - self.delimiters: List[Delimiter] = [] + self.delimiters: list[Delimiter] = [] # Stack of delimiter lists for upper level tags - self._prev_delimiters: List[List[Delimiter]] = [] + self._prev_delimiters: list[list[Delimiter]] = [] # backticklength => last seen position - self.backticks: Dict[int, int] = {} + self.backticks: dict[int, int] = {} self.backticksScanned = False def __repr__(self): diff --git a/markdown_it/rules_inline/strikethrough.py b/markdown_it/rules_inline/strikethrough.py index 87af4b46..fff15897 100644 --- a/markdown_it/rules_inline/strikethrough.py +++ b/markdown_it/rules_inline/strikethrough.py @@ -1,5 +1,6 @@ # ~~strike through~~ -from typing import List +from __future__ import annotations + from .state_inline import StateInline, Delimiter @@ -51,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]): loneMarkers = [] maximum = len(delimiters) diff --git a/markdown_it/token.py b/markdown_it/token.py index 78599a1d..8abf72c3 100644 --- a/markdown_it/token.py +++ b/markdown_it/token.py @@ -1,14 +1,7 @@ -from typing import ( - Any, - Callable, - Dict, - List, - MutableMapping, - Optional, - Tuple, - Type, - Union, -) +from __future__ import annotations + +from collections.abc import Callable, MutableMapping +from typing import Any import warnings import attr @@ -38,15 +31,13 @@ class Token: # - `-1` means the tag is closing nesting: int = attr.ib() # Html attributes. Note this differs from the upstream "list of lists" format - attrs: Dict[str, Union[str, int, float]] = attr.ib( - factory=dict, converter=convert_attrs - ) + attrs: dict[str, str | int | float] = attr.ib(factory=dict, converter=convert_attrs) # Source map info. Format: `[ line_begin, line_end ]` - map: Optional[List[int]] = attr.ib(default=None) + map: list[int] | None = attr.ib(default=None) # nesting level, the same as `state.level` level: int = attr.ib(default=0) # An array of child nodes (inline and img tokens) - children: Optional[List["Token"]] = attr.ib(default=None) + children: list[Token] | None = attr.ib(default=None) # In a case of self-closing tag (code, html, fence, etc.), # it has contents of this tag. content: str = attr.ib(default="") @@ -75,20 +66,20 @@ def attrIndex(self, name: str) -> int: return -1 return list(self.attrs.keys()).index(name) - def attrItems(self) -> List[Tuple[str, Union[str, int, float]]]: + def attrItems(self) -> list[tuple[str, str | int | float]]: """Get (key, value) list of attrs.""" return list(self.attrs.items()) - def attrPush(self, attrData: Tuple[str, Union[str, int, float]]) -> None: + def attrPush(self, attrData: tuple[str, str | int | float]) -> None: """Add `[ name, value ]` attribute to list. Init attrs if necessary.""" name, value = attrData self.attrSet(name, value) - def attrSet(self, name: str, value: Union[str, int, float]) -> None: + def attrSet(self, name: str, value: str | int | float) -> None: """Set `name` attribute to `value`. Override old value if exists.""" self.attrs[name] = value - def attrGet(self, name: str) -> Union[None, str, int, float]: + def attrGet(self, name: str) -> None | str | int | float: """Get the value of attribute `name`, or null if it does not exist.""" return self.attrs.get(name, None) @@ -107,7 +98,7 @@ def attrJoin(self, name: str, value: str) -> None: else: self.attrs[name] = value - def copy(self) -> "Token": + def copy(self) -> Token: """Return a shallow copy of the instance.""" return attr.evolve(self) @@ -116,9 +107,9 @@ def as_dict( *, children: bool = True, as_upstream: bool = True, - meta_serializer: Optional[Callable[[dict], Any]] = None, - filter: Optional[Callable[[attr.Attribute, Any], bool]] = None, - dict_factory: Type[MutableMapping[str, Any]] = dict, + meta_serializer: Callable[[dict], Any] | None = None, + filter: Callable[[attr.Attribute, Any], bool] | None = None, + dict_factory: Callable[..., MutableMapping[str, Any]] = dict, ) -> MutableMapping[str, Any]: """Return the token as a dictionary. @@ -136,7 +127,7 @@ def as_dict( """ mapping = attr.asdict( - self, recurse=False, filter=filter, dict_factory=dict_factory + self, recurse=False, filter=filter, dict_factory=dict_factory # type: ignore[arg-type] ) if as_upstream and "attrs" in mapping: mapping["attrs"] = ( @@ -160,7 +151,7 @@ def as_dict( return mapping @classmethod - def from_dict(cls, dct: MutableMapping[str, Any]) -> "Token": + def from_dict(cls, dct: MutableMapping[str, Any]) -> Token: """Convert a dict to a Token.""" token = cls(**dct) if token.children: @@ -176,17 +167,17 @@ class NestedTokens: opening: Token = attr.ib() closing: Token = attr.ib() - children: List[Union[Token, "NestedTokens"]] = attr.ib(factory=list) + children: list[Token | NestedTokens] = attr.ib(factory=list) def __getattr__(self, name): return getattr(self.opening, name) - def attrGet(self, name: str) -> Union[None, str, int, float]: + def attrGet(self, name: str) -> None | str | int | float: """ Get the value of attribute `name`, or null if it does not exist.""" return self.opening.attrGet(name) -def nest_tokens(tokens: List[Token]) -> List[Union[Token, NestedTokens]]: +def nest_tokens(tokens: list[Token]) -> list[Token | NestedTokens]: """Convert the token stream to a list of tokens and nested tokens. ``NestedTokens`` contain the open and close tokens and a list of children @@ -198,7 +189,7 @@ def nest_tokens(tokens: List[Token]) -> List[Union[Token, NestedTokens]]: DeprecationWarning, ) - output: List[Union[Token, NestedTokens]] = [] + output: list[Token | NestedTokens] = [] tokens = list(reversed(tokens)) while tokens: @@ -210,7 +201,7 @@ def nest_tokens(tokens: List[Token]) -> List[Union[Token, NestedTokens]]: if token.children: # Ignore type checkers because `nest_tokens` doesn't respect # typing of `Token.children`. We add `NestedTokens` into a - # `List[Token]` here. + # `list[Token]` here. token.children = nest_tokens(token.children) # type: ignore continue diff --git a/markdown_it/tree.py b/markdown_it/tree.py index edbc35e0..a3c75487 100644 --- a/markdown_it/tree.py +++ b/markdown_it/tree.py @@ -2,19 +2,15 @@ This module is not part of upstream JavaScript markdown-it. """ +from __future__ import annotations + +from collections.abc import Generator, Sequence import textwrap from typing import ( - Generator, NamedTuple, - Sequence, - Tuple, - Dict, - List, - Optional, Any, TypeVar, overload, - Union, ) from .token import Token @@ -50,10 +46,10 @@ def __init__( If `create_root` is True, create a root node for the document. """ # Only nodes representing an unnested token have self.token - self.token: Optional[Token] = None + self.token: Token | None = None # Only containers have nester tokens - self.nester_tokens: Optional[_NesterTokens] = None + self.nester_tokens: _NesterTokens | None = None # Root node does not have self.parent self._parent: Any = None @@ -92,18 +88,16 @@ def __getitem__(self: _NodeType, item: int) -> _NodeType: ... @overload - def __getitem__(self: _NodeType, item: slice) -> List[_NodeType]: + def __getitem__(self: _NodeType, item: slice) -> list[_NodeType]: ... - def __getitem__( - self: _NodeType, item: Union[int, slice] - ) -> Union[_NodeType, List[_NodeType]]: + def __getitem__(self: _NodeType, item: int | slice) -> _NodeType | list[_NodeType]: return self.children[item] - def to_tokens(self: _NodeType) -> List[Token]: + def to_tokens(self: _NodeType) -> list[Token]: """Recover the linear token stream.""" - def recursive_collect_tokens(node: _NodeType, token_list: List[Token]) -> None: + def recursive_collect_tokens(node: _NodeType, token_list: list[Token]) -> None: if node.type == "root": for child in node.children: recursive_collect_tokens(child, token_list) @@ -116,24 +110,24 @@ def recursive_collect_tokens(node: _NodeType, token_list: List[Token]) -> None: recursive_collect_tokens(child, token_list) token_list.append(node.nester_tokens.closing) - tokens: List[Token] = [] + tokens: list[Token] = [] recursive_collect_tokens(self, tokens) return tokens @property - def children(self: _NodeType) -> List[_NodeType]: + def children(self: _NodeType) -> list[_NodeType]: return self._children @children.setter - def children(self: _NodeType, value: List[_NodeType]) -> None: + def children(self: _NodeType, value: list[_NodeType]) -> None: self._children = value @property - def parent(self: _NodeType) -> Optional[_NodeType]: + def parent(self: _NodeType) -> _NodeType | None: return self._parent @parent.setter - def parent(self: _NodeType, value: Optional[_NodeType]) -> None: + def parent(self: _NodeType, value: _NodeType | None) -> None: self._parent = value @property @@ -178,7 +172,7 @@ def type(self) -> str: return _removesuffix(self.nester_tokens.opening.type, "_open") @property - def next_sibling(self: _NodeType) -> Optional[_NodeType]: + def next_sibling(self: _NodeType) -> _NodeType | None: """Get the next node in the sequence of siblings. Returns `None` if this is the last sibling. @@ -189,7 +183,7 @@ def next_sibling(self: _NodeType) -> Optional[_NodeType]: return None @property - def previous_sibling(self: _NodeType) -> Optional[_NodeType]: + def previous_sibling(self: _NodeType) -> _NodeType | None: """Get the previous node in the sequence of siblings. Returns `None` if this is the first sibling. @@ -286,17 +280,17 @@ def tag(self) -> str: return self._attribute_token().tag @property - def attrs(self) -> Dict[str, Union[str, int, float]]: + def attrs(self) -> dict[str, str | int | float]: """Html attributes.""" return self._attribute_token().attrs - def attrGet(self, name: str) -> Union[None, str, int, float]: + def attrGet(self, name: str) -> None | str | int | float: """Get the value of attribute `name`, or null if it does not exist.""" return self._attribute_token().attrGet(name) @property - def map(self) -> Optional[Tuple[int, int]]: - """Source map info. Format: `Tuple[ line_begin, line_end ]`""" + def map(self) -> tuple[int, int] | None: + """Source map info. Format: `tuple[ line_begin, line_end ]`""" map_ = self._attribute_token().map if map_: # Type ignore because `Token`s attribute types are not perfect diff --git a/markdown_it/utils.py b/markdown_it/utils.py index 5d1ce723..2ba2995a 100644 --- a/markdown_it/utils.py +++ b/markdown_it/utils.py @@ -1,5 +1,7 @@ +from __future__ import annotations + +from collections.abc import Callable from pathlib import Path -from typing import Callable, List, Optional, Union class OptionsDict(dict): @@ -78,16 +80,16 @@ def langPrefix(self, value: str): self["langPrefix"] = value @property - def highlight(self) -> Optional[Callable[[str, str, str], str]]: + def highlight(self) -> Callable[[str, str, str], str] | None: """Highlighter function: (content, langName, langAttrs) -> escaped HTML.""" return self["highlight"] @highlight.setter - def highlight(self, value: Optional[Callable[[str, str, str], str]]): + def highlight(self, value: Callable[[str, str, str], str] | None): self["highlight"] = value -def read_fixture_file(path: Union[str, Path]) -> List[list]: +def read_fixture_file(path: str | Path) -> list[list]: text = Path(path).read_text(encoding="utf-8") tests = [] section = 0 diff --git a/setup.cfg b/setup.cfg index f8bd8e69..652b36a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,6 @@ classifiers = Intended Audience :: Developers License :: OSI Approved :: MIT License Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -33,7 +32,7 @@ install_requires = attrs>=19,<22 mdurl~=0.1 typing_extensions>=3.7.4;python_version<'3.8' -python_requires = ~=3.6 +python_requires = >=3.7 include_package_data = True zip_safe = False diff --git a/tox.ini b/tox.ini index c169f982..e771e3e9 100644 --- a/tox.ini +++ b/tox.ini @@ -9,13 +9,13 @@ envlist = py37 [testenv] usedevelop = true -[testenv:py{36,37,38,39,310}] +[testenv:py{37,38,39,310}] extras = linkify testing commands = pytest {posargs:tests/} -[testenv:py{36,37,38,39,310}-plugins] +[testenv:py{37,38,39,310}-plugins] extras = testing changedir = {envtmpdir} allowlist_externals = @@ -27,11 +27,11 @@ commands_pre = commands = pytest {posargs} -[testenv:py{36,37,38,39}-bench-core] +[testenv:py{37,38,39}-bench-core] extras = benchmarking commands = pytest benchmarking/bench_core.py {posargs} -[testenv:py{36,37,38}-bench-packages] +[testenv:py{37,38}-bench-packages] extras = benchmarking,compare commands = pytest benchmarking/bench_packages.py {posargs}