Skip to content

Move from attrs to dataclasses #200

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,3 @@ repos:
rev: v0.812
hooks:
- id: mypy
additional_dependencies: [attrs]
10 changes: 10 additions & 0 deletions markdown_it/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from __future__ import annotations

from collections.abc import Mapping
import sys
from typing import Any

if sys.version_info >= (3, 10):
dataclass_kwargs: Mapping[str, Any] = {"slots": True}
else:
dataclass_kwargs: Mapping[str, Any] = {}
16 changes: 9 additions & 7 deletions markdown_it/ruler.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ class Ruler
from __future__ import annotations

from collections.abc import Callable, Iterable, MutableMapping
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
import attr

from markdown_it._compat import dataclass_kwargs

if TYPE_CHECKING:
from markdown_it import MarkdownIt
Expand Down Expand Up @@ -50,12 +52,12 @@ def src(self, value: str) -> None:
RuleFunc = Callable


@attr.s(slots=True)
@dataclass(**dataclass_kwargs)
class Rule:
name: str = attr.ib()
enabled: bool = attr.ib()
fn: RuleFunc = attr.ib(repr=False)
alt: list[str] = attr.ib()
name: str
enabled: bool
fn: RuleFunc = field(repr=False)
alt: list[str]


class Ruler:
Expand Down Expand Up @@ -105,7 +107,7 @@ def at(self, ruleName: str, fn: RuleFunc, options=None):
options = options or {}
if index == -1:
raise KeyError(f"Parser rule not found: {ruleName}")
self.__rules__[index].fn = fn
self.__rules__[index].fn = fn # type: ignore[assignment]
self.__rules__[index].alt = options.get("alt", [])
self.__cache__ = None

Expand Down
22 changes: 11 additions & 11 deletions markdown_it/rules_inline/state_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from collections import namedtuple
from collections.abc import MutableMapping
from typing import TYPE_CHECKING
from dataclasses import dataclass

import attr

from .._compat import dataclass_kwargs
from ..token import Token
from ..ruler import StateBase
from ..common.utils import isWhiteSpace, isPunctChar, isMdAsciiPunct
Expand All @@ -14,35 +14,35 @@
from markdown_it import MarkdownIt


@attr.s(slots=True)
@dataclass(**dataclass_kwargs)
class Delimiter:
# Char code of the starting marker (number).
marker: int = attr.ib()
marker: int

# Total length of these series of delimiters.
length: int = attr.ib()
length: int

# An amount of characters before this one that's equivalent to
# current one. In plain English: if this delimiter does not open
# an emphasis, neither do previous `jump` characters.
#
# Used to skip sequences like "*****" in one step, for 1st asterisk
# value will be 0, for 2nd it's 1 and so on.
jump: int = attr.ib()
jump: int

# A position of the token this delimiter corresponds to.
token: int = attr.ib()
token: int

# If this delimiter is matched as a valid opener, `end` will be
# equal to its position, otherwise it's `-1`.
end: int = attr.ib()
end: int

# Boolean flags that determine if this delimiter could open or close
# an emphasis.
open: bool = attr.ib()
close: bool = attr.ib()
open: bool
close: bool

level: bool = attr.ib(default=None)
level: bool | None = None


Scanned = namedtuple("Scanned", ["can_open", "can_close", "length"])
Expand Down
94 changes: 29 additions & 65 deletions markdown_it/token.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

from collections.abc import Callable, MutableMapping
import copy
import dataclasses
from dataclasses import dataclass, field
from typing import Any
import warnings

import attr
from markdown_it._compat import dataclass_kwargs


def convert_attrs(value: Any) -> Any:
Expand All @@ -19,43 +22,46 @@ def convert_attrs(value: Any) -> Any:
return value


@attr.s(slots=True)
@dataclass(**dataclass_kwargs)
class Token:
# Type of the token (string, e.g. "paragraph_open")
type: str = attr.ib()
type: str
# html tag name, e.g. "p"
tag: str = attr.ib()
tag: str
# Level change (number in {-1, 0, 1} set), where:
# - `1` means the tag is opening
# - `0` means the tag is self-closing
# - `-1` means the tag is closing
nesting: int = attr.ib()
nesting: int
# Html attributes. Note this differs from the upstream "list of lists" format
attrs: dict[str, str | int | float] = attr.ib(factory=dict, converter=convert_attrs)
attrs: dict[str, str | int | float] = field(default_factory=dict)
# Source map info. Format: `[ line_begin, line_end ]`
map: list[int] | None = attr.ib(default=None)
map: list[int] | None = None
# nesting level, the same as `state.level`
level: int = attr.ib(default=0)
level: int = 0
# An array of child nodes (inline and img tokens)
children: list[Token] | None = attr.ib(default=None)
children: list[Token] | None = None
# In a case of self-closing tag (code, html, fence, etc.),
# it has contents of this tag.
content: str = attr.ib(default="")
content: str = ""
# '*' or '_' for emphasis, fence string for fence, etc.
markup: str = attr.ib(default="")
markup: str = ""
# Additional information:
# - Info string for "fence" tokens
# - The value "auto" for autolink "link_open" and "link_close" tokens
# - The string value of the item marker for ordered-list "list_item_open" tokens
info: str = attr.ib(default="")
info: str = ""
# A place for plugins to store any arbitrary data
meta: dict = attr.ib(factory=dict)
meta: dict = field(default_factory=dict)
# True for block-level tokens, false for inline tokens.
# Used in renderer to calculate line breaks
block: bool = attr.ib(default=False)
block: bool = False
# If it's true, ignore this element when rendering.
# Used for tight lists to hide paragraphs.
hidden: bool = attr.ib(default=False)
hidden: bool = False

def __post_init__(self):
self.attrs = convert_attrs(self.attrs)

def attrIndex(self, name: str) -> int:
warnings.warn(
Expand Down Expand Up @@ -100,55 +106,13 @@ def attrJoin(self, name: str, value: str) -> None:

def copy(self) -> Token:
"""Return a shallow copy of the instance."""
return attr.evolve(self)
return copy.copy(self)

def as_dict(
self,
*,
children: bool = True,
as_upstream: bool = True,
meta_serializer: Callable[[dict], Any] | None = None,
filter: Callable[[attr.Attribute, Any], bool] | None = None,
dict_factory: Callable[..., MutableMapping[str, Any]] = dict,
self, *, dict_factory: Callable[..., MutableMapping[str, Any]] = dict
) -> MutableMapping[str, Any]:
"""Return the token as a dictionary.

:param children: Also convert children to dicts
:param as_upstream: Ensure the output dictionary is equal to that created by markdown-it
For example, attrs are converted to null or lists
:param meta_serializer: hook for serializing ``Token.meta``
:param filter: A callable whose return code determines whether an
attribute or element is included (``True``) or dropped (``False``).
Is called with the `attr.Attribute` as the first argument and the
value as the second argument.
:param dict_factory: A callable to produce dictionaries from.
For example, to produce ordered dictionaries instead of normal Python
dictionaries, pass in ``collections.OrderedDict``.

"""
mapping = attr.asdict(
self, recurse=False, filter=filter, dict_factory=dict_factory # type: ignore[arg-type]
)
if as_upstream and "attrs" in mapping:
mapping["attrs"] = (
None
if not mapping["attrs"]
else [[k, v] for k, v in mapping["attrs"].items()]
)
if meta_serializer and "meta" in mapping:
mapping["meta"] = meta_serializer(mapping["meta"])
if children and mapping.get("children", None):
mapping["children"] = [
child.as_dict(
children=children,
filter=filter,
dict_factory=dict_factory,
as_upstream=as_upstream,
meta_serializer=meta_serializer,
)
for child in mapping["children"]
]
return mapping
"""Return the token as a dictionary."""
return dataclasses.asdict(self, dict_factory=dict_factory)

@classmethod
def from_dict(cls, dct: MutableMapping[str, Any]) -> Token:
Expand All @@ -159,15 +123,15 @@ def from_dict(cls, dct: MutableMapping[str, Any]) -> Token:
return token


@attr.s(slots=True)
@dataclass(**dataclass_kwargs)
class NestedTokens:
"""A class that closely resembles a Token,
but for a an opening/closing Token pair, and their containing children.
"""

opening: Token = attr.ib()
closing: Token = attr.ib()
children: list[Token | NestedTokens] = attr.ib(factory=list)
opening: Token
closing: Token
children: list[Token | NestedTokens] = field(default_factory=list)

def __getattr__(self, name):
return getattr(self.opening, name)
Expand Down
1 change: 0 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ project_urls =
[options]
packages = find:
install_requires =
attrs>=19,<22
mdurl~=0.1
typing_extensions>=3.7.4;python_version<'3.8'
python_requires = >=3.7
Expand Down
Loading